Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[APP-1049] show label expiration in frontend (#7738)

* Add support for label exp to LabelsOnMeDialog

* Add exp to PostAlerts, align with LabelsOnMe UI

* Bump weight

* Improve translations

* Expiry should round up

* Add a little visual alignment hack

authored by

Eric Bailey and committed by
GitHub
da45c42b 63ba0a43

+145 -49
+37 -12
src/components/moderation/LabelsOnMeDialog.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 import {useMutation} from '@tanstack/react-query' 7 7 8 + import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 8 9 import {useLabelSubject} from '#/lib/moderation' 9 10 import {useLabelInfo} from '#/lib/moderation/useLabelInfo' 10 11 import {makeProfileLink} from '#/lib/routes/links' ··· 66 67 /> 67 68 ) : ( 68 69 <> 69 - <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> 70 + <Text style={[a.text_2xl, a.font_heavy, a.pb_xs, a.leading_tight]}> 70 71 {isAccount ? ( 71 72 <Trans>Labels on your account</Trans> 72 73 ) : ( ··· 122 123 const sourceName = labeler 123 124 ? sanitizeHandle(labeler.creator.handle, '@') 124 125 : label.src 126 + const timeDiff = useGetTimeAgo({future: true}) 125 127 return ( 126 128 <View 127 129 style={[ ··· 163 165 <Trans>This label was applied by you.</Trans> 164 166 </Text> 165 167 ) : ( 166 - <View style={{flexDirection: 'row'}}> 167 - <Text style={[t.atoms.text_contrast_medium]}> 168 - <Trans>Source: </Trans>{' '} 168 + <View 169 + style={[ 170 + a.flex_row, 171 + a.justify_between, 172 + a.gap_xl, 173 + {paddingBottom: 1}, 174 + ]}> 175 + <Text 176 + style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]} 177 + numberOfLines={1}> 178 + <Trans> 179 + Source:{' '} 180 + <InlineLinkText 181 + label={sourceName} 182 + to={makeProfileLink( 183 + labeler ? labeler.creator : {did: label.src, handle: ''}, 184 + )} 185 + onPress={() => control.close()}> 186 + {sourceName} 187 + </InlineLinkText> 188 + </Trans> 169 189 </Text> 170 - <InlineLinkText 171 - label={sourceName} 172 - to={makeProfileLink( 173 - labeler ? labeler.creator : {did: label.src, handle: ''}, 174 - )} 175 - onPress={() => control.close()}> 176 - {sourceName} 177 - </InlineLinkText> 190 + {label.exp && ( 191 + <View> 192 + <Text 193 + style={[ 194 + a.leading_snug, 195 + a.text_sm, 196 + a.italic, 197 + t.atoms.text_contrast_medium, 198 + ]}> 199 + <Trans>Expires in {timeDiff(Date.now(), label.exp)}</Trans> 200 + </Text> 201 + </View> 202 + )} 178 203 </View> 179 204 )} 180 205 </View>
+78 -25
src/components/moderation/ModerationDetailsDialog.tsx
··· 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 + import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 6 7 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 7 8 import {makeProfileLink} from '#/lib/routes/links' 8 9 import {listUriToHref} from '#/lib/strings/url-helpers' 9 10 import {isNative} from '#/platform/detection' 10 11 import {useSession} from '#/state/session' 11 - import {atoms as a, useTheme} from '#/alf' 12 + import {atoms as a, useGutters, useTheme} from '#/alf' 12 13 import * as Dialog from '#/components/Dialog' 13 - import {Divider} from '#/components/Divider' 14 14 import {InlineLinkText} from '#/components/Link' 15 15 import {AppModerationCause} from '#/components/Pills' 16 16 import {Text} from '#/components/Typography' ··· 38 38 control: Dialog.DialogOuterProps['control'] 39 39 }) { 40 40 const t = useTheme() 41 + const xGutters = useGutters([0, 'base']) 41 42 const {_} = useLingui() 42 43 const desc = useModerationCauseDescription(modcause) 43 44 const {currentAccount} = useSession() 45 + const timeDiff = useGetTimeAgo({future: true}) 44 46 45 47 let name 46 48 let description ··· 128 130 description = '' 129 131 } 130 132 133 + const sourceName = 134 + desc.source || desc.sourceDisplayName || _(msg`an unknown labeler`) 135 + 131 136 return ( 132 - <Dialog.ScrollableInner label={_(msg`Moderation details`)}> 133 - <Text emoji style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}> 134 - {name} 135 - </Text> 136 - <Text style={[t.atoms.text, a.text_md, a.leading_snug]}> 137 - {description} 138 - </Text> 137 + <Dialog.ScrollableInner 138 + label={_(msg`Moderation details`)} 139 + contentContainerStyle={{ 140 + paddingLeft: 0, 141 + paddingRight: 0, 142 + paddingBottom: 0, 143 + }}> 144 + <View style={[xGutters, a.pb_lg]}> 145 + <Text emoji style={[t.atoms.text, a.text_2xl, a.font_heavy, a.mb_sm]}> 146 + {name} 147 + </Text> 148 + <Text style={[t.atoms.text, a.text_sm, a.leading_snug]}> 149 + {description} 150 + </Text> 151 + </View> 139 152 140 153 {modcause?.type === 'label' && ( 141 - <View style={[a.pt_lg]}> 142 - <Divider /> 154 + <View 155 + style={[ 156 + xGutters, 157 + a.py_md, 158 + a.border_t, 159 + !isNative && t.atoms.bg_contrast_25, 160 + t.atoms.border_contrast_low, 161 + { 162 + borderBottomLeftRadius: a.rounded_md.borderRadius, 163 + borderBottomRightRadius: a.rounded_md.borderRadius, 164 + }, 165 + ]}> 143 166 {modcause.source.type === 'user' ? ( 144 - <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> 167 + <Text style={[t.atoms.text, a.text_md, a.leading_snug]}> 145 168 <Trans>This label was applied by the author.</Trans> 146 169 </Text> 147 170 ) : ( 148 171 <> 149 - <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> 150 - <Trans> 151 - This label was applied by{' '} 152 - <InlineLinkText 153 - label={desc.source || _(msg`an unknown labeler`)} 154 - to={makeProfileLink({did: modcause.label.src, handle: ''})} 155 - onPress={() => control.close()} 156 - style={a.text_md}> 157 - {desc.source || _(msg`an unknown labeler`)} 158 - </InlineLinkText> 159 - . 160 - </Trans> 161 - </Text> 172 + <View 173 + style={[ 174 + a.flex_row, 175 + a.justify_between, 176 + a.gap_xl, 177 + {paddingBottom: 1}, 178 + ]}> 179 + <Text 180 + style={[ 181 + a.flex_1, 182 + a.leading_snug, 183 + t.atoms.text_contrast_medium, 184 + ]} 185 + numberOfLines={1}> 186 + <Trans> 187 + Source:{' '} 188 + <InlineLinkText 189 + label={sourceName} 190 + to={makeProfileLink({ 191 + did: modcause.label.src, 192 + handle: '', 193 + })} 194 + onPress={() => control.close()}> 195 + {sourceName} 196 + </InlineLinkText> 197 + </Trans> 198 + </Text> 199 + {modcause.label.exp && ( 200 + <View> 201 + <Text 202 + style={[ 203 + a.leading_snug, 204 + a.text_sm, 205 + a.italic, 206 + t.atoms.text_contrast_medium, 207 + ]}> 208 + <Trans> 209 + Expires in {timeDiff(Date.now(), modcause.label.exp)} 210 + </Trans> 211 + </Text> 212 + </View> 213 + )} 214 + </View> 162 215 </> 163 216 )} 164 217 </View>
+20 -7
src/lib/hooks/useTimeAgo.ts
··· 19 19 const DAY = HOUR * 24 20 20 const MONTH_30 = DAY * 30 21 21 22 - export function useGetTimeAgo() { 22 + export function useGetTimeAgo({future = false}: {future?: boolean} = {}) { 23 23 const {i18n} = useLingui() 24 24 return useCallback( 25 25 ( ··· 27 27 later: number | string | Date, 28 28 options?: {format: DateDiffFormat}, 29 29 ) => { 30 - const diff = dateDiff(earlier, later) 30 + const diff = dateDiff(earlier, later, future ? 'up' : 'down') 31 31 return formatDateDiff({diff, i18n, format: options?.format}) 32 32 }, 33 - [i18n], 33 + [i18n, future], 34 34 ) 35 35 } 36 36 ··· 45 45 export function dateDiff( 46 46 earlier: number | string | Date, 47 47 later: number | string | Date, 48 + rounding: 'up' | 'down' = 'down', 48 49 ): DateDiff { 49 50 let diff = { 50 51 value: 0, ··· 65 66 unit: 'second' as DateDiff['unit'], 66 67 } 67 68 } else if (diffSeconds < HOUR) { 68 - const value = Math.floor(diffSeconds / MINUTE) 69 + const value = 70 + rounding === 'up' 71 + ? Math.ceil(diffSeconds / MINUTE) 72 + : Math.floor(diffSeconds / MINUTE) 69 73 diff = { 70 74 value, 71 75 unit: 'minute' as DateDiff['unit'], 72 76 } 73 77 } else if (diffSeconds < DAY) { 74 - const value = Math.floor(diffSeconds / HOUR) 78 + const value = 79 + rounding === 'up' 80 + ? Math.ceil(diffSeconds / HOUR) 81 + : Math.floor(diffSeconds / HOUR) 75 82 diff = { 76 83 value, 77 84 unit: 'hour' as DateDiff['unit'], 78 85 } 79 86 } else if (diffSeconds < MONTH_30) { 80 - const value = Math.floor(diffSeconds / DAY) 87 + const value = 88 + rounding === 'up' 89 + ? Math.ceil(diffSeconds / DAY) 90 + : Math.floor(diffSeconds / DAY) 81 91 diff = { 82 92 value, 83 93 unit: 'day' as DateDiff['unit'], 84 94 } 85 95 } else { 86 - const value = Math.floor(diffSeconds / MONTH_30) 96 + const value = 97 + rounding === 'up' 98 + ? Math.ceil(diffSeconds / MONTH_30) 99 + : Math.floor(diffSeconds / MONTH_30) 87 100 diff = { 88 101 value, 89 102 unit: 'month' as DateDiff['unit'],
+10 -5
src/lib/moderation/useModerationCauseDescription.ts
··· 7 7 import {msg} from '@lingui/macro' 8 8 import {useLingui} from '@lingui/react' 9 9 10 + import {sanitizeHandle} from '#/lib/strings/handles' 10 11 import {useLabelDefinitions} from '#/state/preferences' 11 12 import {useSession} from '#/state/session' 12 13 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' ··· 23 24 name: string 24 25 description: string 25 26 source?: string 27 + sourceDisplayName?: string 26 28 sourceType?: ModerationCauseSource['type'] 27 29 sourceAvi?: string 28 30 sourceDid?: string ··· 130 132 const def = cause.labelDef || getDefinition(labelDefs, cause.label) 131 133 const strings = getLabelStrings(i18n.locale, globalLabelStrings, def) 132 134 const labeler = labelers.find(l => l.creator.did === cause.label.src) 133 - let source = 134 - labeler?.creator.displayName || 135 - (labeler?.creator.handle ? '@' + labeler?.creator.handle : undefined) 135 + let source = labeler 136 + ? sanitizeHandle(labeler.creator.handle, '@') 137 + : undefined 138 + let sourceDisplayName = labeler?.creator.displayName 136 139 if (!source) { 137 140 if (cause.label.src === BSKY_LABELER_DID) { 138 - source = 'Bluesky Moderation Service' 141 + source = 'moderation.bsky.app' 142 + sourceDisplayName = 'Bluesky Moderation Service' 139 143 } else { 140 - source = cause.label.src 144 + source = _(msg`an unknown labeler`) 141 145 } 142 146 } 143 147 if (def.identifier === 'porn' || def.identifier === 'sexual') { ··· 154 158 name: strings.name, 155 159 description: strings.description, 156 160 source, 161 + sourceDisplayName, 157 162 sourceType: cause.source.type, 158 163 sourceAvi: labeler?.creator.avatar, 159 164 sourceDid: cause.label.src,