An ATproto social media client -- with an independent Appview.
6
fork

Configure Feed

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

Add WIP 'report post' modal

+211 -2
+11 -1
src/state/models/shell-ui.ts
··· 51 51 } 52 52 } 53 53 54 + export class ReportPostModal { 55 + name = 'report-post' 56 + 57 + constructor(public postUrl: string) { 58 + makeAutoObservable(this) 59 + } 60 + } 61 + 54 62 interface LightboxModel { 55 63 canSwipeLeft: boolean 56 64 canSwipeRight: boolean ··· 127 135 | EditProfileModal 128 136 | CreateSceneModal 129 137 | ServerInputModal 138 + | ReportPostModal 130 139 | undefined 131 140 isLightboxActive = false 132 141 activeLightbox: ··· 154 163 | ConfirmModal 155 164 | EditProfileModal 156 165 | CreateSceneModal 157 - | ServerInputModal, 166 + | ServerInputModal 167 + | ReportPostModal, 158 168 ) { 159 169 this.isModalActive = true 160 170 this.activeModal = modal
+8
src/view/com/modals/Modal.tsx
··· 12 12 import * as CreateSceneModal from './CreateScene' 13 13 import * as InviteToSceneModal from './InviteToScene' 14 14 import * as ServerInputModal from './ServerInput' 15 + import * as ReportPostModal from './ReportPost' 15 16 16 17 const CLOSED_SNAPPOINTS = ['10%'] 17 18 ··· 68 69 element = ( 69 70 <ServerInputModal.Component 70 71 {...(store.shell.activeModal as models.ServerInputModal)} 72 + /> 73 + ) 74 + } else if (store.shell.activeModal?.name === 'report-post') { 75 + snapPoints = ReportPostModal.snapPoints 76 + element = ( 77 + <ReportPostModal.Component 78 + {...(store.shell.activeModal as models.ReportPostModal)} 71 79 /> 72 80 ) 73 81 } else {
+94
src/view/com/modals/ReportPost.tsx
··· 1 + import React, {useState} from 'react' 2 + import { 3 + ActivityIndicator, 4 + StyleSheet, 5 + Text, 6 + TouchableOpacity, 7 + View, 8 + } from 'react-native' 9 + import LinearGradient from 'react-native-linear-gradient' 10 + import {useStores} from '../../../state' 11 + import {s, colors, gradients} from '../../lib/styles' 12 + import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup' 13 + import {ErrorMessage} from '../util/ErrorMessage' 14 + 15 + const ITEMS: RadioGroupItem[] = [ 16 + {key: 'spam', label: 'Spam or excessive repeat posts'}, 17 + {key: 'abuse', label: 'Abusive, rude, or hateful'}, 18 + {key: 'copyright', label: 'Contains copyrighted material'}, 19 + {key: 'illegal', label: 'Contains illegal content'}, 20 + ] 21 + 22 + export const snapPoints = ['50%'] 23 + 24 + export function Component({postUrl}: {postUrl: string}) { 25 + const store = useStores() 26 + const [isProcessing, setIsProcessing] = useState<boolean>(false) 27 + const [error, setError] = useState<string>('') 28 + const [issue, setIssue] = useState<string>('') 29 + const onSelectIssue = (v: string) => setIssue(v) 30 + const onPress = async () => { 31 + setError('') 32 + setIsProcessing(true) 33 + try { 34 + // TODO 35 + store.shell.closeModal() 36 + return 37 + } catch (e: any) { 38 + setError(e.toString()) 39 + setIsProcessing(false) 40 + } 41 + } 42 + return ( 43 + <View style={[s.flex1, s.pl10, s.pr10]}> 44 + <Text style={styles.title}>Report post</Text> 45 + <Text style={styles.description}>What is the issue with this post?</Text> 46 + <RadioGroup items={ITEMS} onSelect={onSelectIssue} /> 47 + {error ? ( 48 + <View style={s.mt10}> 49 + <ErrorMessage message={error} /> 50 + </View> 51 + ) : undefined} 52 + {isProcessing ? ( 53 + <View style={[styles.btn, s.mt10]}> 54 + <ActivityIndicator /> 55 + </View> 56 + ) : issue ? ( 57 + <TouchableOpacity style={s.mt10} onPress={onPress}> 58 + <LinearGradient 59 + colors={[gradients.primary.start, gradients.primary.end]} 60 + start={{x: 0, y: 0}} 61 + end={{x: 1, y: 1}} 62 + style={[styles.btn]}> 63 + <Text style={[s.white, s.bold, s.f18]}>Send Report</Text> 64 + </LinearGradient> 65 + </TouchableOpacity> 66 + ) : undefined} 67 + </View> 68 + ) 69 + } 70 + 71 + const styles = StyleSheet.create({ 72 + title: { 73 + textAlign: 'center', 74 + fontWeight: 'bold', 75 + fontSize: 24, 76 + marginBottom: 12, 77 + }, 78 + description: { 79 + textAlign: 'center', 80 + fontSize: 17, 81 + paddingHorizontal: 22, 82 + color: colors.gray5, 83 + marginBottom: 10, 84 + }, 85 + btn: { 86 + flexDirection: 'row', 87 + alignItems: 'center', 88 + justifyContent: 'center', 89 + width: '100%', 90 + borderRadius: 32, 91 + padding: 14, 92 + backgroundColor: colors.gray1, 93 + }, 94 + })
+8 -1
src/view/com/util/DropdownBtn.tsx
··· 15 15 import {colors} from '../../lib/styles' 16 16 import {toShareUrl} from '../../../lib/strings' 17 17 import {useStores} from '../../../state' 18 - import {ConfirmModal} from '../../../state/models/shell-ui' 18 + import {ReportPostModal, ConfirmModal} from '../../../state/models/shell-ui' 19 19 import {TABS_ENABLED} from '../../../build-flags' 20 20 21 21 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} ··· 114 114 label: 'Share...', 115 115 onPress() { 116 116 Share.share({url: toShareUrl(itemHref)}) 117 + }, 118 + }, 119 + { 120 + icon: 'circle-exclamation', 121 + label: 'Report post', 122 + onPress() { 123 + store.shell.openModal(new ReportPostModal(itemHref)) 117 124 }, 118 125 }, 119 126 isAuthor
+54
src/view/com/util/forms/RadioButton.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' 3 + import {colors} from '../../../lib/styles' 4 + 5 + export function RadioButton({ 6 + label, 7 + isSelected, 8 + onPress, 9 + }: { 10 + label: string 11 + isSelected: boolean 12 + onPress: () => void 13 + }) { 14 + return ( 15 + <TouchableOpacity style={styles.outer} onPress={onPress}> 16 + <View style={styles.circle}> 17 + {isSelected ? <View style={styles.circleFill} /> : undefined} 18 + </View> 19 + <Text style={styles.label}>{label}</Text> 20 + </TouchableOpacity> 21 + ) 22 + } 23 + 24 + const styles = StyleSheet.create({ 25 + outer: { 26 + flexDirection: 'row', 27 + alignItems: 'center', 28 + marginBottom: 5, 29 + borderRadius: 8, 30 + borderWidth: 1, 31 + borderColor: colors.gray2, 32 + paddingHorizontal: 10, 33 + paddingVertical: 8, 34 + }, 35 + circle: { 36 + width: 30, 37 + height: 30, 38 + borderRadius: 15, 39 + padding: 4, 40 + borderWidth: 1, 41 + borderColor: colors.gray3, 42 + marginRight: 10, 43 + }, 44 + circleFill: { 45 + width: 20, 46 + height: 20, 47 + borderRadius: 10, 48 + backgroundColor: colors.blue3, 49 + }, 50 + label: { 51 + flex: 1, 52 + fontSize: 17, 53 + }, 54 + })
+34
src/view/com/util/forms/RadioGroup.tsx
··· 1 + import React, {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {RadioButton} from './RadioButton' 4 + 5 + export interface RadioGroupItem { 6 + label: string 7 + key: string 8 + } 9 + 10 + export function RadioGroup({ 11 + items, 12 + onSelect, 13 + }: { 14 + items: RadioGroupItem[] 15 + onSelect: (key: string) => void 16 + }) { 17 + const [selection, setSelection] = useState<string>('') 18 + const onSelectInner = (key: string) => { 19 + setSelection(key) 20 + onSelect(key) 21 + } 22 + return ( 23 + <View> 24 + {items.map(item => ( 25 + <RadioButton 26 + key={item.key} 27 + label={item.label} 28 + isSelected={item.key === selection} 29 + onPress={() => onSelectInner(item.key)} 30 + /> 31 + ))} 32 + </View> 33 + ) 34 + }
+2
src/view/index.ts
··· 21 21 import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera' 22 22 import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' 23 23 import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' 24 + import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation' 24 25 import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser' 25 26 import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' 26 27 import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone' ··· 86 87 faCamera, 87 88 faCheck, 88 89 faCircleCheck, 90 + faCircleExclamation, 89 91 faCircleUser, 90 92 faClone, 91 93 farClone,