Bluesky app fork with some witchin' additions 💫

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 records.map(record => createDel(record.uri)), 218 ), 219 }) 220 } 221 222 async subscribe() {
··· 217 records.map(record => createDel(record.uri)), 218 ), 219 }) 220 + 221 + this.rootStore.emitListDeleted(this.uri) 222 } 223 224 async subscribe() {
+15
src/state/models/lists/lists-list.ts
··· 48 return this.hasLoaded && !this.hasContent 49 } 50 51 // public api 52 // = 53 54 async refresh() { 55 return this.loadMore(true)
··· 48 return this.hasLoaded && !this.hasContent 49 } 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 + 58 // public api 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 + } 68 69 async refresh() { 70 return this.loadMore(true)
+8
src/state/models/root-store.ts
··· 188 DeviceEventEmitter.emit('post-deleted', uri) 189 } 190 191 // the session has started and been fully hydrated 192 onSessionLoaded(handler: () => void): EmitterSubscription { 193 return DeviceEventEmitter.addListener('session-loaded', handler)
··· 188 DeviceEventEmitter.emit('post-deleted', uri) 189 } 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 + 199 // the session has started and been fully hydrated 200 onSessionLoaded(handler: () => void): EmitterSubscription { 201 return DeviceEventEmitter.addListener('session-loaded', handler)
+4 -1
src/state/models/ui/profile.ts
··· 87 } 88 89 get selectedView() { 90 - return this.selectorItems[this.selectedViewIndex] 91 } 92 93 get uiItems() {
··· 87 } 88 89 get selectedView() { 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 94 } 95 96 get uiItems() {
+23 -11
src/view/com/modals/ListAddRemoveUser.tsx
··· 20 import {s} from 'lib/styles' 21 import {usePalette} from 'lib/hooks/usePalette' 22 import {isDesktopWeb, isAndroid} from 'platform/detection' 23 24 export const snapPoints = ['fullscreen'] 25 ··· 37 const pal = usePalette('default') 38 const palPrimary = usePalette('primary') 39 const palInverted = usePalette('inverted') 40 const [selected, setSelected] = React.useState<string[]>([]) 41 42 const listsList: ListsListModel = React.useMemo( ··· 51 listsList.refresh() 52 memberships.fetch().then( 53 () => { 54 - setSelected(memberships.memberships.map(m => m.value.list)) 55 }, 56 err => { 57 store.log.error('Failed to fetch memberships', {err}) ··· 156 ) 157 }, [onPressNewMuteList]) 158 159 return ( 160 <View testID="listAddRemoveUserModal" style={s.hContentRegion}> 161 <Text style={[styles.title, pal.text]}>Add {displayName} to Lists</Text> ··· 178 onAccessibilityEscape={onPressCancel} 179 label="Cancel" 180 /> 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 </View> 192 </View> 193 )
··· 20 import {s} from 'lib/styles' 21 import {usePalette} from 'lib/hooks/usePalette' 22 import {isDesktopWeb, isAndroid} from 'platform/detection' 23 + import isEqual from 'lodash.isequal' 24 25 export const snapPoints = ['fullscreen'] 26 ··· 38 const pal = usePalette('default') 39 const palPrimary = usePalette('primary') 40 const palInverted = usePalette('inverted') 41 + const [originalSelections, setOriginalSelections] = React.useState< 42 + string[] 43 + >([]) 44 const [selected, setSelected] = React.useState<string[]>([]) 45 46 const listsList: ListsListModel = React.useMemo( ··· 55 listsList.refresh() 56 memberships.fetch().then( 57 () => { 58 + const ids = memberships.memberships.map(m => m.value.list) 59 + setOriginalSelections(ids) 60 + setSelected(ids) 61 }, 62 err => { 63 store.log.error('Failed to fetch memberships', {err}) ··· 162 ) 163 }, [onPressNewMuteList]) 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 + 169 return ( 170 <View testID="listAddRemoveUserModal" style={s.hContentRegion}> 171 <Text style={[styles.title, pal.text]}>Add {displayName} to Lists</Text> ··· 188 onAccessibilityEscape={onPressCancel} 189 label="Cancel" 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 + )} 203 </View> 204 </View> 205 )
+8
src/view/screens/Profile.tsx
··· 56 setHasSetup(false) 57 }, [route.params.name]) 58 59 useFocusEffect( 60 React.useCallback(() => { 61 const softResetSub = store.onScreenSoftReset(onSoftReset) ··· 126 /> 127 ) 128 }, [uiState, onRefresh, route.params.hideBackButton]) 129 const Footer = React.useMemo(() => { 130 return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined 131 }, [uiState.showLoadingMoreFooter])
··· 56 setHasSetup(false) 57 }, [route.params.name]) 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 + 66 useFocusEffect( 67 React.useCallback(() => { 68 const softResetSub = store.onScreenSoftReset(onSoftReset) ··· 133 /> 134 ) 135 }, [uiState, onRefresh, route.params.hideBackButton]) 136 + 137 const Footer = React.useMemo(() => { 138 return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined 139 }, [uiState.showLoadingMoreFooter])