@@ -3,6 +3,7 @@
// E v e r y h e x v a l u e h e r e i s f r o m t h e s p e c ; n o t h i n g i s i n v e n t e d .
import SwiftUI
import UIKit
// MARK: - H e x c o l o r s
@@ -24,28 +25,63 @@ extension Color {
}
}
extension UIColor {
convenience init ( hex : String ) {
let s = hex . trimmingCharacters ( in : CharacterSet ( charactersIn : " # " ) )
var v : UInt64 = 0
Scanner ( string : s ) . scanHexInt64 ( & v )
let r , g , b : CGFloat
switch s . count {
case 6 :
r = CGFloat ( ( v >> 16 ) & 0xFF ) / 255
g = CGFloat ( ( v >> 8 ) & 0xFF ) / 255
b = CGFloat ( v & 0xFF ) / 255
default :
r = 0 ; g = 0 ; b = 0
}
self . init ( red : r , green : g , blue : b , alpha : 1 )
}
}
// MARK: - P a l e t t e
enum Palette {
static let orange = Color ( " KisaniOrange " ) // # F F 5 C 0 0
static let black = Color . black // # 0 0 0 0 0 0
static let surface = Color ( hex : " 111111 " )
static let surface2 = Color ( hex : " 1A1A1A " )
static let hairline = Color ( hex : " 1A1A1A " )
static let orange = Color ( " KisaniOrange " ) // # F F 5 C 0 0
static func adaptive ( dark : String , light : String ) -> Color {
Color ( UIColor { traits in
traits . userInterfaceStyle = = . dark ? UIColor ( hex : dark ) : UI Color( hex : light )
} )
}
static let background = adaptive ( dark : " 0A0A0A " , light : " F7F4EF " )
static let black = background
static let surface = adaptive ( dark : " 111111 " , light : " EFE9DD " )
static let surface2 = adaptive ( dark : " 1A1A1A " , light : " E3DBCE " )
static let hairline = adaptive ( dark : " 1A1A1A " , light : " D6CEC0 " )
static let primaryText = adaptive ( dark : " F5F2ED " , light : " 171411 " )
static let secondaryText = adaptive ( dark : " 9A9A9A " , light : " 4F4941 " )
static let tertiaryText = adaptive ( dark : " 666666 " , light : " 6F675E " )
static let mutedText = adaptive ( dark : " 4A4A4A " , light : " 948B80 " )
static let chipFill = adaptive ( dark : " 242424 " , light : " DDD3C1 " )
static let chipText = adaptive ( dark : " E0E0E0 " , light : " 28231E " )
static let chipMutedText = adaptive ( dark : " 787878 " , light : " 68625A " )
// h e a l t h
static let healthActive = Color ( hex : " 2A5A2A " )
static let healthFailing = Color ( hex : " AA6600 " )
static let healthDead = Color ( hex : " 5A1A1A " )
static let healthActive = adaptive ( dark : " 2A5A2A " , light : " DCEBDD " )
static let healthFailing = adaptive ( dark : " AA6600 " , light : " F2D6A6 " )
static let healthDead = adaptive ( dark : " 5A1A1A " , light : " E9C7C7 " )
// b l o c k s
static let consensusBorder = Color ( hex : " FF5C00 " )
static let consensusFill = Color ( hex : " 1F1206 " ) // d a r k o r a n g e
static let conflictBorder = Color ( hex : " 5A1A1A " ) // d a r k r e d
static let conflictFill = Color ( hex : " 1A0C0C " ) // d a r k r e d
static let consensusFill = adaptive ( dark : " 1F1206 " , light : " FFE2CA " )
static let conflictBorder = adaptive ( dark : " 5A1A1A " , light : " BF5B5B " )
static let conflictFill = adaptive ( dark : " 1A0C0C " , light : " F3D8D8 " )
static let cachedGreen = Color ( hex : " 2A5A2A " )
static let bodyText = Color ( hex : " 4A4A4A " )
static let cachedGreen = adaptive ( dark : " 2A5A2A " , light : " 2C6A38 " )
static let bodyText = adaptive ( dark : " 8A8A8A " , light : " 4A453F " )
}
// MARK: - S i g n a l - s c o r e f a d e
@@ -67,6 +103,30 @@ enum Signal {
opacity : 1 )
}
private static func lerpTuple ( _ a : ( Double , Double , Double ) ,
_ b : ( Double , Double , Double ) ,
_ t : Double ) -> ( Double , Double , Double ) {
let t = max ( 0 , min ( 1 , t ) )
return (
a . 0 + ( b . 0 - a . 0 ) * t ,
a . 1 + ( b . 1 - a . 1 ) * t ,
a . 2 + ( b . 2 - a . 2 ) * t
)
}
private static func adaptiveLerp ( _ darkA : ( Double , Double , Double ) ,
_ darkB : ( Double , Double , Double ) ,
_ lightA : ( Double , Double , Double ) ,
_ lightB : ( Double , Double , Double ) ,
_ t : Double ) -> Color {
Color ( UIColor { traits in
let rgb = traits . userInterfaceStyle = = . dark
? lerpTuple ( darkA , darkB , t )
: lerpTuple ( lightA , lightB , t )
return UIColor ( red : rgb . 0 , green : rgb . 1 , blue : rgb . 2 , alpha : 1 )
} )
}
// / 3 p t l e f t s t r i p e c o l o r : # 1 A 1 A 1 A ( 0 ) → # F F 5 C 0 0 ( 9 7 + ) .
static func stripeColor ( _ score : Int ) -> Color {
let t = Double ( score ) / 97.0
@@ -77,18 +137,21 @@ enum Signal {
// / S t o r y t i t l e c o l o r : w h i t e ( 9 7 + ) → # 4 4 4 4 4 4 ( 2 0 ) → n e a r i n v i s i b l e ( 0 ) .
static func titleColor ( _ score : Int ) -> Color {
if score >= 97 { return . white }
if score >= 97 { return Palette . primaryText }
if score >= 20 {
let t = Double ( score - 20 ) / Double ( 97 - 20 )
return lerp ( ( 0x44 / 255 , 0x44 / 255 , 0x44 / 255 ) , // # 4 4 4 4 4 4
( 1 , 1 , 1 ) , // w h i t e
t )
return adaptiveLerp (
( 0x44 / 255 , 0x44 / 255 , 0x44 / 255 ) , ( 0xF5 / 255 , 0xF2 / 255 , 0xED / 255 ) ,
( 0x8A / 255 , 0x81 / 255 , 0x76 / 255 ) , ( 0x17 / 255 , 0x14 / 255 , 0x11 / 255 ) ,
t
)
}
// 0 – 1 9 n e a r i n v i s i b l e : # 1 C 1 C 1 C → # 4 4 4 4 4 4
let t = Double ( score ) / 20.0
return lerp ( ( 0x1C / 255 , 0x1C / 255 , 0x1C / 255 ) ,
( 0x44 / 255 , 0x44 / 255 , 0x44 / 255 ) ,
t )
return adaptiveLerp (
( 0x1C / 255 , 0x1C / 255 , 0x1C / 255 ) , ( 0x44 / 255 , 0x44 / 255 , 0x44 / 255 ) ,
( 0xD6 / 255 , 0xCE / 255 , 0xC0 / 255 ) , ( 0x8A / 255 , 0x81 / 255 , 0x76 / 255 ) ,
t
)
}
// / S i g n a l s c o r e n u m b e r c o l o r : s t a y s i n o r a n g e / b r o w n h u e , f a d e s w i t h s c o r e .
@@ -195,14 +258,31 @@ enum AppearanceMode: String, CaseIterable {
}
}
// MARK: - S t a b l e f e e d o r d e r i n g
// MARK: - F r e s h n e s s - w e i g h t e d f e e d o r d e r i n g
//
// T h e s e r v e r a l r e a d y a p p l i e s f r e s h n e s s w h e n c o m p u t i n g s i g n a l S c o r e , b u t t h a t
// s n a p s h o t a g e s o n c e i t ' s c a c h e d o n t h e c l i e n t . c l i e n t R a n k a p p l i e s a l i g h t w e i g h t
// t i m e - d e c a y s o f r e s h s t o r i e s n a t u r a l l y s u r f a c e o v e r s t a l e h i g h - s c o r e r s .
//
// F o r m u l a : r a n k = s c o r e × ( 0 . 8 0 + 0 . 2 0 × d e c a y )
// d e c a y = 1 / ( 1 + a g e H o u r s / 2 4 ) → 1 . 0 n o w · 0 . 5 0 @ 2 4 h · 0 . 3 3 @ 4 8 h
//
// E f f e c t : a s c o r e - 8 0 s t o r y f r o m 1 h o u r a g o ( r a n k ≈ 7 9 . 8 ) b e a t s a s c o r e - 8 2 s t o r y
// f r o m 4 8 h o u r s a g o ( r a n k ≈ 7 1 . 4 ) . P u r e s c o r e s t i l l d o m i n a t e s f o r s a m e - a g e s t o r i e s .
extension StorySummary {
// / D e t e r m i n i s t i c r a n k i n g : s i g n a l s c o r e d e s c e n d i n g , t h e n i d d e s c e n d i n g — t h e
// / s a m e o r d e r t h e b a c k e n d u s e s ( ` s i g n a l _ s c o r e D E S C , i d D E S C ` ) . W i t h o u t t h e
// / i d t i e b r e a k e r , e q u a l - s c o r e s t o r i e s r e s h u f f l e o n e v e r y ( u n s t a b l e ) r e - s o r t .
var clientRank : Double {
let ageHours = max ( 0.0 , - updatedAt . timeIntervalSinceNow ) / 3600.0
let decay = 1.0 / ( 1.0 + ageHours / 24.0 )
return Double ( signalScore ) * ( 0.80 + 0.20 * decay )
}
static func feedOrder ( _ a : StorySummary , _ b : StorySummary ) -> Bool {
a . signalScore != b . signalScore ? a . signalScore > b . signalScore : a . id > b . id
let diff = a . clientRank - b . clientRank
// S t a b l e t i e b r e a k f o r n e a r l y - e q u a l r a n k s : n e w e r I D w i n s ( I D s a r e
// l e x i c o g r a p h i c a l l y m o n o t o n e f r o m t h e b a c k e n d ) .
if abs ( diff ) < 0.5 { return a . id > b . id }
return diff > 0
}
}
@@ -216,9 +296,9 @@ enum StoryPill: String, CaseIterable, Identifiable {
case all = " All "
case f1 = " F1 "
case sport = " Sport "
case tech = " Tech " // c o v e r s A I , C l o u d , H o m e L a b — u s e s u b - s e c t i o n s i n s i d e
case ugand a = " Uganda "
case southAfrica = " South Africa "
case tech = " Tech " // A I , C l o u d , H o m e L a b — s u b - s e c t i o n s i n s i d e
case eastAfric a = " East Africa " // U g a n d a + b r o a d e r E a s t A f r i c a , U g a n d a p i n n e d f i r s t
case southern Africa = " Southern Africa " // S A , N a m i b i a , B o t s w a n a p i n n e d f i r s t
case canada = " Canada "
case unitedStates = " US "
@@ -227,28 +307,33 @@ enum StoryPill: String, CaseIterable, Identifiable {
var slugs : Set < String > {
switch self {
case . all : return [ ]
case . f1 : return [ " formula-1 " ]
case . sport : return [ " sports " , " esports " ]
case . tech : return [
case . all : return [ ]
case . f1 : return [ " formula-1 " ]
case . sport : return [ " sports " , " esports " ]
case . tech : return [
" technology " , " programming-and-software-development " ,
" cybersecurity " , " security " , " privacy-and-data-protection " ,
" web-design-and-ui-ux " , " wordpress-and-web-development " ,
" artificial-intelligence " , " machine-learning " , " robotics " ,
" cloud-computing " , " homelab " ,
]
case . ugand a: return [ " uganda " ]
case . southAfrica : return [ " south-africa " ]
case . canada : return [ " c anada " ]
case . unitedStates : return [ " united-states " ]
case . eastAfric a: return [ " east-africa " , " uganda " ]
case . southern Africa : return [
" south-africa " , " namibia " , " botsw ana " ,
" zimbabwe " , " zambia " , " mozambique " , " southern-africa " ,
]
case . canada : return [ " canada " ]
case . unitedStates : return [ " united-states " ]
}
}
// / S u b - s e c t i o n s t o s h o w i n s i d e t h e c a r d l a y o u t f o r t h i s p i l l ( n i l = f l a t ) .
var subSections : [ NewsSection ] ? {
switch self {
case . tech : return NewsSection . techSubSections
default : return nil
case . tech : return NewsSection . techSubSections
case . eastAfrica : return NewsSection . eastAfricaSubSections
case . southernAfrica : return NewsSection . southernAfricaSubSections
default : return nil
}
}
}
@@ -278,7 +363,7 @@ struct JarvisWordmark: View {
var body : some View {
HStack ( spacing : 0 ) {
Text ( " j " ) . font ( font ) . foregroundStyle ( Palette . orange )
Text ( " arvis " ) . font ( font ) . foregroundStyle ( . white )
Text ( " arvis " ) . font ( font ) . foregroundStyle ( Palette . primaryText )
}
}
}
@@ -300,7 +385,9 @@ struct NewsSection: Identifiable {
. init ( id : " politics " , label : " Politics " , slugs : [ " politics " , " human-rights " , " investigative " ] , pill : nil ) ,
. init ( id : " business " , label : " Business " , slugs : [ " business " , " finance " , " fintech " , " cryptocurrency " , " blockchain " , " entrepreneur " , " startups " , " real-estate " ] , pill : nil ) ,
. init ( id : " tech " , label : " Tech " , slugs : [ " technology " , " programming-and-software-development " , " cybersecurity " , " security " , " web-design-and-ui-ux " , " wordpress-and-web-development " , " privacy-and-data-protection " , " cloud-computing " , " artificial-intelligence " , " machine-learning " , " robotics " , " homelab " ] , pill : . tech ) ,
. init ( id : " africa " , label : " Africa " , slugs : [ " africa " , " east-africa" , " south-africa " , " uganda " ] , pill : nil ) ,
. init ( id : " east- africa" , label : " East Africa" , slugs : [ " east-africa " , " uganda " ] , pill : . eastAfrica ) ,
. init ( id : " southern-africa " , label : " Southern Africa " , slugs : [ " south-africa " , " namibia " , " botswana " , " zimbabwe " , " zambia " , " mozambique " , " southern-africa " ] , pill : . southernAfrica ) ,
. init ( id : " africa " , label : " Africa " , slugs : [ " africa " ] , pill : nil ) ,
. init ( id : " sport " , label : " Sport " , slugs : [ " sports " , " esports " , " formula-1 " ] , pill : . sport ) ,
. init ( id : " science " , label : " Science " , slugs : [ " science " , " astronomy " , " environment " , " green-technology " , " sustainability " ] , pill : nil ) ,
. init ( id : " entertainment " , label : " Entertainment " , slugs : [ " entertainment " , " music " , " pop-culture " , " video-game " , " arts-and-culture " , " books-and-literature " ] , pill : nil ) ,
@@ -317,4 +404,20 @@ struct NewsSection: Identifiable {
. init ( id : " t-dev " , label : " Dev " , slugs : [ " programming-and-software-development " , " web-design-and-ui-ux " , " wordpress-and-web-development " ] , pill : nil ) ,
. init ( id : " t-tech " , label : " Technology " , slugs : [ " technology " ] , pill : nil ) ,
]
// / S u b - s e c t i o n s s h o w n i n s i d e t h e E a s t A f r i c a p i l l v i e w — U g a n d a p i n n e d f i r s t .
static let eastAfricaSubSections : [ NewsSection ] = [
. init ( id : " ea-uganda " , label : " Uganda " , slugs : [ " uganda " ] , pill : nil ) ,
. init ( id : " ea-kenya " , label : " Kenya " , slugs : [ " east-africa " ] , pill : nil ) ,
]
// / S u b - s e c t i o n s s h o w n i n s i d e t h e S o u t h e r n A f r i c a p i l l — S A , N a m i b i a , B o t s w a n a f i r s t .
static let southernAfricaSubSections : [ NewsSection ] = [
. init ( id : " sa-za " , label : " South Africa " , slugs : [ " south-africa " ] , pill : nil ) ,
. init ( id : " sa-na " , label : " Namibia " , slugs : [ " namibia " ] , pill : nil ) ,
. init ( id : " sa-bw " , label : " Botswana " , slugs : [ " botswana " ] , pill : nil ) ,
. init ( id : " sa-zw " , label : " Zimbabwe " , slugs : [ " zimbabwe " ] , pill : nil ) ,
. init ( id : " sa-zm " , label : " Zambia " , slugs : [ " zambia " ] , pill : nil ) ,
. init ( id : " sa-mz " , label : " Mozambique " , slugs : [ " mozambique " ] , pill : nil ) ,
]
}