1import * as Constants from '../constants/tracker2'
2import * as Types from '../constants/types/tracker2'
3import * as ConfigGen from '../actions/config-gen'
4import * as Tracker2Gen from '../actions/tracker2-gen'
5import * as Container from '../util/container'
6import * as EngineGen from '../actions/engine-gen-gen'
7import * as RpcTypes from '../constants/types/rpc-gen'
8import {mapGetEnsureValue} from '../util/map'
9import logger from '../logger'
10
11const initialState: Types.State = Constants.makeState()
12
13const getDetails = (state: Types.State, username: string) =>
14  mapGetEnsureValue(state.usernameToDetails, username, {...Constants.noDetails})
15
16type Actions =
17  | Tracker2Gen.Actions
18  | ConfigGen.BootstrapStatusLoadedPayload
19  | EngineGen.Keybase1NotifyTrackingNotifyUserBlockedPayload
20  | EngineGen.Keybase1Identify3UiIdentify3UpdateRowPayload
21  | EngineGen.Keybase1Identify3UiIdentify3UserResetPayload
22  | EngineGen.Keybase1Identify3UiIdentify3UpdateUserCardPayload
23  | EngineGen.Keybase1Identify3UiIdentify3SummaryPayload
24
25export default Container.makeReducer<Actions, Types.State>(initialState, {
26  [Tracker2Gen.resetStore]: () => initialState,
27  [ConfigGen.bootstrapStatusLoaded]: (draftState, action) => {
28    const {username, fullname} = action.payload
29    getDetails(draftState, username).fullname = fullname
30  },
31  [Tracker2Gen.load]: (draftState, action) => {
32    const {guiID, forceDisplay, assertion, reason} = action.payload
33    const username = assertion
34    if (forceDisplay) {
35      logger.info(`Showing tracker for assertion: ${assertion}`)
36    }
37    const d = getDetails(draftState, username)
38    d.assertions = new Map() // just remove for now, maybe keep them
39    d.guiID = guiID
40    d.reason = reason
41    d.showTracker = forceDisplay || d.showTracker // show it or keep the last state
42    d.state = 'checking'
43    d.username = username
44  },
45  [Tracker2Gen.updateResult]: (draftState, action) => {
46    const {guiID} = action.payload
47    const username = Constants.guiIDToUsername(draftState, guiID)
48    if (!username) {
49      return
50    }
51
52    const {reason, result} = action.payload
53    const newReason =
54      reason ||
55      (result === 'broken' && `Some of ${username}'s proofs have changed since you last followed them.`)
56
57    const d = getDetails(draftState, username)
58    // Don't overwrite the old reason if the user reset.
59    if (!d.resetBrokeTrack || d.reason.length === 0) {
60      d.reason = newReason || d.reason
61    }
62    if (result === 'valid') {
63      d.resetBrokeTrack = false
64    }
65    d.state = result
66  },
67  [Tracker2Gen.closeTracker]: (draftState, action) => {
68    const {guiID} = action.payload
69    const username = Constants.guiIDToUsername(draftState, guiID)
70    if (!username) {
71      return
72    }
73
74    logger.info(`Closing tracker for assertion: ${username}`)
75    const d = getDetails(draftState, username)
76    d.showTracker = false
77  },
78  [Tracker2Gen.updateFollows]: (draftState, action) => {
79    const {username, followers, following} = action.payload
80    const d = getDetails(draftState, username)
81    if (followers) {
82      d.followers = new Set(followers.map(f => f.username))
83      d.followersCount = d.followers.size
84    }
85    if (following) {
86      d.following = new Set(following.map(f => f.username))
87      d.followingCount = d.following.size
88    }
89  },
90  [Tracker2Gen.updateWotEntries]: (draftState, action) => {
91    const d = getDetails(draftState, action.payload.voucheeUsername)
92    d.webOfTrustEntries = action.payload.entries
93  },
94  [Tracker2Gen.proofSuggestionsUpdated]: (draftState, action) => {
95    draftState.proofSuggestions = Container.castDraft(action.payload.suggestions)
96  },
97  [Tracker2Gen.loadedNonUserProfile]: (draftState, action) => {
98    const {assertion, ...rest} = action.payload
99    const {usernameToNonUserDetails} = draftState
100    const old = usernameToNonUserDetails.get(assertion) || Constants.noNonUserDetails
101    usernameToNonUserDetails.set(assertion, {
102      ...old,
103      ...rest,
104    })
105  },
106  // This allows the server to send us a notification to *remove* (not add)
107  // arbitrary followers from arbitrary tracker2 results, so we can hide
108  // blocked users from follower lists.
109  [EngineGen.keybase1NotifyTrackingNotifyUserBlocked]: (draftState, action) => {
110    const {blocker, blocks} = action.payload.params.b
111    const d = getDetails(draftState, blocker)
112    const toProcess = Object.entries(blocks ?? {}).map(
113      ([username, userBlocks]) => [username, getDetails(draftState, username), userBlocks || []] as const
114    )
115    toProcess.forEach(([username, det, userBlocks]) => {
116      userBlocks.forEach(blockState => {
117        if (blockState.blockType === RpcTypes.UserBlockType.chat) {
118          det.blocked = blockState.blocked
119        } else if (blockState.blockType === RpcTypes.UserBlockType.follow) {
120          det.hidFromFollowers = blockState.blocked
121          blockState.blocked && d.followers && d.followers.delete(username)
122        }
123      })
124    })
125    d.followersCount = d.followers?.size
126  },
127  [EngineGen.keybase1Identify3UiIdentify3UpdateRow]: (draftState, action) => {
128    const {row} = action.payload.params
129    const {guiID} = row
130    const username = Constants.guiIDToUsername(draftState, guiID)
131    if (!username) {
132      return
133    }
134
135    const d = getDetails(draftState, username)
136    const assertions = d.assertions ?? new Map()
137    d.assertions = assertions
138    const assertion = Constants.rpcAssertionToAssertion(row)
139    assertions.set(assertion.assertionKey, assertion)
140  },
141  [EngineGen.keybase1Identify3UiIdentify3UserReset]: (draftState, action) => {
142    const {guiID} = action.payload.params
143    const username = Constants.guiIDToUsername(draftState, guiID)
144    if (!username) {
145      return
146    }
147
148    const d = getDetails(draftState, username)
149    d.resetBrokeTrack = true
150    d.reason = `${username} reset their account since you last followed them.`
151  },
152  [EngineGen.keybase1Identify3UiIdentify3UpdateUserCard]: (draftState, action) => {
153    const {guiID, card} = action.payload.params
154    const username = Constants.guiIDToUsername(draftState, guiID)
155    if (!username) {
156      return
157    }
158
159    const {bio, blocked, fullName, hidFromFollowers, location, stellarHidden, teamShowcase} = card
160    const {unverifiedNumFollowers, unverifiedNumFollowing} = card
161    const d = getDetails(draftState, username)
162    d.bio = bio
163    d.blocked = blocked
164    // These will be overridden by a later updateFollows, if it happens (will
165    // happen when viewing profile, but not in tracker pop up.
166    d.followersCount = unverifiedNumFollowers
167    d.followingCount = unverifiedNumFollowing
168    d.fullname = fullName
169    d.location = location
170    d.stellarHidden = stellarHidden
171    d.teamShowcase =
172      teamShowcase?.map(t => ({
173        description: t.description,
174        isOpen: t.open,
175        membersCount: t.numMembers,
176        name: t.fqName,
177        publicAdmins: t.publicAdmins ?? [],
178      })) ?? []
179    d.hidFromFollowers = hidFromFollowers
180  },
181  [EngineGen.keybase1Identify3UiIdentify3Summary]: (draftState, action) => {
182    const {summary} = action.payload.params
183    const {numProofsToCheck, guiID} = summary
184    const username = Constants.guiIDToUsername(draftState, guiID)
185    if (!username) {
186      return
187    }
188
189    const d = getDetails(draftState, username)
190    d.numAssertionsExpected = numProofsToCheck
191  },
192})
193