my fork of the bluesky client

List cleanup on remove (#1069)

* :lipstick: Hide Add to List option on own profile

* :sparkles: Remove Lists tab when last list is removed

* :sparkles: Add listener to list delete on profile screen

* :sparkles: Only show save changes in list modal when changes are made

authored by

Foysal Ahamed and committed by
GitHub
eec300d7 38d78e16

+60 -12
+2
src/state/models/content/list.ts
··· 217 217 records.map(record => createDel(record.uri)), 218 218 ), 219 219 }) 220 + 221 + this.rootStore.emitListDeleted(this.uri) 220 222 } 221 223 222 224 async subscribe() {
+15
src/state/models/lists/lists-list.ts
··· 48 48 return this.hasLoaded && !this.hasContent 49 49 } 50 50 51 + /** 52 + * Removes posts from the feed upon deletion. 53 + */ 54 + onListDeleted(uri: string) { 55 + this.lists = this.lists.filter(l => l.uri !== uri) 56 + } 57 + 51 58 // public api 52 59 // = 60 + 61 + /** 62 + * Register any event listeners. Returns a cleanup function. 63 + */ 64 + registerListeners() { 65 + const sub = this.rootStore.onListDeleted(this.onListDeleted.bind(this)) 66 + return () => sub.remove() 67 + } 53 68 54 69 async refresh() { 55 70 return this.loadMore(true)
+8
src/state/models/root-store.ts
··· 188 188 DeviceEventEmitter.emit('post-deleted', uri) 189 189 } 190 190 191 + // a list was deleted by the local user 192 + onListDeleted(handler: (uri: string) => void): EmitterSubscription { 193 + return DeviceEventEmitter.addListener('list-deleted', handler) 194 + } 195 + emitListDeleted(uri: string) { 196 + DeviceEventEmitter.emit('list-deleted', uri) 197 + } 198 + 191 199 // the session has started and been fully hydrated 192 200 onSessionLoaded(handler: () => void): EmitterSubscription { 193 201 return DeviceEventEmitter.addListener('session-loaded', handler)
+4 -1
src/state/models/ui/profile.ts
··· 87 87 } 88 88 89 89 get selectedView() { 90 - return this.selectorItems[this.selectedViewIndex] 90 + // If, for whatever reason, the selected view index is not available, default back to posts 91 + // This can happen when the user was focused on a view but performed an action that caused 92 + // the view to disappear (e.g. deleting the last list in their list of lists https://imgflip.com/i/7txu1y) 93 + return this.selectorItems[this.selectedViewIndex] || Sections.Posts 91 94 } 92 95 93 96 get uiItems() {
+23 -11
src/view/com/modals/ListAddRemoveUser.tsx
··· 20 20 import {s} from 'lib/styles' 21 21 import {usePalette} from 'lib/hooks/usePalette' 22 22 import {isDesktopWeb, isAndroid} from 'platform/detection' 23 + import isEqual from 'lodash.isequal' 23 24 24 25 export const snapPoints = ['fullscreen'] 25 26 ··· 37 38 const pal = usePalette('default') 38 39 const palPrimary = usePalette('primary') 39 40 const palInverted = usePalette('inverted') 41 + const [originalSelections, setOriginalSelections] = React.useState< 42 + string[] 43 + >([]) 40 44 const [selected, setSelected] = React.useState<string[]>([]) 41 45 42 46 const listsList: ListsListModel = React.useMemo( ··· 51 55 listsList.refresh() 52 56 memberships.fetch().then( 53 57 () => { 54 - setSelected(memberships.memberships.map(m => m.value.list)) 58 + const ids = memberships.memberships.map(m => m.value.list) 59 + setOriginalSelections(ids) 60 + setSelected(ids) 55 61 }, 56 62 err => { 57 63 store.log.error('Failed to fetch memberships', {err}) ··· 156 162 ) 157 163 }, [onPressNewMuteList]) 158 164 165 + // Only show changes button if there are some items on the list to choose from AND user has made changes in selection 166 + const canSaveChanges = 167 + !listsList.isEmpty && !isEqual(selected, originalSelections) 168 + 159 169 return ( 160 170 <View testID="listAddRemoveUserModal" style={s.hContentRegion}> 161 171 <Text style={[styles.title, pal.text]}>Add {displayName} to Lists</Text> ··· 178 188 onAccessibilityEscape={onPressCancel} 179 189 label="Cancel" 180 190 /> 181 - <Button 182 - testID="saveBtn" 183 - type="primary" 184 - onPress={onPressSave} 185 - style={styles.footerBtn} 186 - accessibilityLabel="Save changes" 187 - accessibilityHint="" 188 - onAccessibilityEscape={onPressSave} 189 - label="Save Changes" 190 - /> 191 + {canSaveChanges && ( 192 + <Button 193 + testID="saveBtn" 194 + type="primary" 195 + onPress={onPressSave} 196 + style={styles.footerBtn} 197 + accessibilityLabel="Save changes" 198 + accessibilityHint="" 199 + onAccessibilityEscape={onPressSave} 200 + label="Save Changes" 201 + /> 202 + )} 191 203 </View> 192 204 </View> 193 205 )
+8
src/view/screens/Profile.tsx
··· 56 56 setHasSetup(false) 57 57 }, [route.params.name]) 58 58 59 + // We don't need this to be reactive, so we can just register the listeners once 60 + useEffect(() => { 61 + const listCleanup = uiState.lists.registerListeners() 62 + return () => listCleanup() 63 + // eslint-disable-next-line react-hooks/exhaustive-deps 64 + }, []) 65 + 59 66 useFocusEffect( 60 67 React.useCallback(() => { 61 68 const softResetSub = store.onScreenSoftReset(onSoftReset) ··· 126 133 /> 127 134 ) 128 135 }, [uiState, onRefresh, route.params.hideBackButton]) 136 + 129 137 const Footer = React.useMemo(() => { 130 138 return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined 131 139 }, [uiState.showLoadingMoreFooter])