feat(mobile): keep team, template, and report flows in sync with the server
Team, meeting, memo, template, command, and dictionary screens drifted from the server contract, and report submission could hang instead of confirming to the user. The screens now use the server responses directly. The retired Expo shell is removed; the React Native app is the mobile client. Gradle-generated vector-icon drawables are ignored rather than committed.
This commit is contained in:
parent
bb0e54dcee
commit
94d8bb8ebe
32 changed files with 310 additions and 2229 deletions
|
|
@ -840,7 +840,7 @@ function createStyles(palette: Palette) {
|
|||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 18,
|
||||
backgroundColor: 'rgba(0,0,0,0.72)',
|
||||
backgroundColor: palette.scrim,
|
||||
},
|
||||
modalCard: { maxHeight: '88%', padding: 0, overflow: 'hidden' },
|
||||
modalContent: { padding: 18, gap: 16 },
|
||||
|
|
|
|||
|
|
@ -603,7 +603,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
output: { gap: 7, padding: 12, borderWidth: 1, borderRadius: 9, borderColor: palette.accent.main },
|
||||
outputHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
||||
outputText: { lineHeight: 21 },
|
||||
backdrop: { flex: 1, justifyContent: 'flex-end', padding: 18, backgroundColor: 'rgba(0,0,0,0.65)' },
|
||||
backdrop: { flex: 1, justifyContent: 'flex-end', padding: 18, backgroundColor: palette.scrim },
|
||||
editor: { gap: 13, maxHeight: '92%' },
|
||||
editorActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 9 },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -673,7 +673,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: 'rgba(0,0,0,0.72)',
|
||||
backgroundColor: palette.scrim,
|
||||
},
|
||||
modalCard: {
|
||||
width: '100%',
|
||||
|
|
|
|||
|
|
@ -1152,7 +1152,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.62)',
|
||||
backgroundColor: palette.scrim,
|
||||
},
|
||||
modalCard: { gap: 14, width: '100%', maxWidth: 640, alignSelf: 'center' },
|
||||
editorInput: {
|
||||
|
|
|
|||
|
|
@ -772,7 +772,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.62)',
|
||||
backgroundColor: palette.scrim,
|
||||
},
|
||||
modalCard: { gap: 14, maxWidth: 560, maxHeight: '92%', width: '100%', alignSelf: 'center' },
|
||||
modalScroll: { flexShrink: 1 },
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
tag: { minHeight: 38, justifyContent: 'center', paddingHorizontal: 10, borderRadius: 999, backgroundColor: palette.accent.dim },
|
||||
addRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
addInput: { flex: 1, marginHorizontal: 0 },
|
||||
modalBackdrop: { flex: 1, justifyContent: 'center', padding: 20, backgroundColor: 'rgba(0,0,0,0.68)' },
|
||||
modalBackdrop: { flex: 1, justifyContent: 'center', padding: 20, backgroundColor: palette.scrim },
|
||||
modalCard: { width: '100%', maxWidth: 560, alignSelf: 'center', gap: 14 },
|
||||
actions: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
flexButton: { flex: 1 },
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { useMobilePreferences } from '../lib/preferences-context'
|
|||
import { ThemeButton, ThemeCard, ThemeText } from '../theme/themed-components'
|
||||
import {
|
||||
cancelTeamInvite,
|
||||
createTeamActivity,
|
||||
createTeamInvite,
|
||||
deleteTeamRevisionSafe,
|
||||
getTeamDetail,
|
||||
|
|
@ -70,6 +71,8 @@ export default function TeamDetailScreen({ route }: TeamDetailScreenProps): Reac
|
|||
const [inviteRole, setInviteRole] = useState<'admin' | 'member'>('member')
|
||||
const [editorError, setEditorError] = useState<TeamServiceErrorCode | null>(null)
|
||||
const [inviteResult, setInviteResult] = useState<TeamInviteResult | null>(null)
|
||||
const [activityBody, setActivityBody] = useState('')
|
||||
const [postingActivity, setPostingActivity] = useState(false)
|
||||
|
||||
const load = useCallback(async (pull = false): Promise<void> => {
|
||||
const requestGeneration = ++generation.current
|
||||
|
|
@ -123,6 +126,29 @@ export default function TeamDetailScreen({ route }: TeamDetailScreenProps): Reac
|
|||
const canAdminister = detail?.currentRole === 'owner' || detail?.currentRole === 'admin'
|
||||
const isOwner = detail?.currentRole === 'owner'
|
||||
|
||||
const memberNameById = useMemo(() => {
|
||||
const map = new Map<string, string>()
|
||||
for (const member of detail?.members ?? []) {
|
||||
map.set(member.userId, memberLabel(member, t('mobile.teams.unnamedMember')))
|
||||
}
|
||||
return map
|
||||
}, [detail, t])
|
||||
|
||||
const postActivity = async (): Promise<void> => {
|
||||
const body = activityBody.trim()
|
||||
if (user === null || body.length === 0 || postingActivity) return
|
||||
setPostingActivity(true)
|
||||
try {
|
||||
await createTeamActivity(teamId, 'note', body)
|
||||
setActivityBody('')
|
||||
await load(false)
|
||||
} catch (postError) {
|
||||
Alert.alert(t('mobile.teams.activity'), teamErrorMessage(teamErrorCode(postError), t))
|
||||
} finally {
|
||||
setPostingActivity(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openRename = (): void => {
|
||||
if (detail === null) return
|
||||
setName(detail.team.name)
|
||||
|
|
@ -523,6 +549,45 @@ export default function TeamDetailScreen({ route }: TeamDetailScreenProps): Reac
|
|||
))}
|
||||
</ThemeCard>
|
||||
|
||||
<ThemeCard style={styles.section} testID="team-activity">
|
||||
<ThemeText variant="heading">{t('mobile.teams.activity')}</ThemeText>
|
||||
<TextInput
|
||||
value={activityBody}
|
||||
onChangeText={setActivityBody}
|
||||
placeholder={t('mobile.teams.activityPlaceholder')}
|
||||
multiline
|
||||
maxLength={2000}
|
||||
style={styles.activityInput}
|
||||
testID="team-activity-input"
|
||||
/>
|
||||
<ThemeButton
|
||||
label={postingActivity ? t('mobile.teams.posting') : t('mobile.teams.post')}
|
||||
variant="secondary"
|
||||
disabled={postingActivity || activityBody.trim().length === 0}
|
||||
onPress={() => { void postActivity() }}
|
||||
testID="team-activity-post"
|
||||
/>
|
||||
{detail.activities.length === 0 ? (
|
||||
<ThemeText color="muted">{t('mobile.teams.noActivity')}</ThemeText>
|
||||
) : detail.activities.map((activity) => (
|
||||
<View key={activity.id} style={styles.activityRow} testID={`team-activity-${activity.id}`}>
|
||||
<ThemeText variant="small" color="muted">
|
||||
{activity.actorId !== null
|
||||
? memberNameById.get(activity.actorId) ?? t('mobile.teams.unnamedMember')
|
||||
: t('mobile.teams.system')}
|
||||
{' · '}
|
||||
{formatDate(new Date(activity.createdAt), {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
</ThemeText>
|
||||
{activity.body !== null && activity.body.length > 0 && (
|
||||
<ThemeText>{activity.body}</ThemeText>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</ThemeCard>
|
||||
|
||||
<ThemeCard style={styles.section}>
|
||||
<ThemeText variant="heading">{t('mobile.teams.management')}</ThemeText>
|
||||
{isOwner ? (
|
||||
|
|
@ -746,11 +811,30 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
backgroundColor: palette.bg.inset,
|
||||
gap: 5,
|
||||
},
|
||||
activityInput: {
|
||||
minHeight: 52,
|
||||
maxHeight: 140,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: palette.border.strong,
|
||||
borderRadius: 10,
|
||||
backgroundColor: palette.bg.inset,
|
||||
color: palette.text.primary,
|
||||
fontSize: 15,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
activityRow: {
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
backgroundColor: palette.bg.inset,
|
||||
gap: 4,
|
||||
},
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: 'rgba(0,0,0,0.62)',
|
||||
backgroundColor: palette.scrim,
|
||||
},
|
||||
modalCard: { width: '100%', maxWidth: 560, alignSelf: 'center', gap: 14 },
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: 'rgba(0,0,0,0.62)',
|
||||
backgroundColor: palette.scrim,
|
||||
},
|
||||
modalCard: { width: '100%', maxWidth: 560, alignSelf: 'center', gap: 14 },
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@ function createStyles(palette: Palette): ReturnType<typeof StyleSheet.create> {
|
|||
badge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 999, backgroundColor: palette.accent.dim },
|
||||
actions: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
flexButton: { flex: 1 },
|
||||
modalBackdrop: { flex: 1, justifyContent: 'center', padding: 16, backgroundColor: 'rgba(0,0,0,0.68)' },
|
||||
modalBackdrop: { flex: 1, justifyContent: 'center', padding: 16, backgroundColor: palette.scrim },
|
||||
modalList: { width: '100%', maxWidth: 680, maxHeight: '94%', alignSelf: 'center', borderRadius: 14, backgroundColor: palette.bg.card },
|
||||
modalCard: { padding: 18, gap: 14 },
|
||||
editorSection: { gap: 14 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue