Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/containers/RoomItem/RoomItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { type IRoomItemProps } from './interfaces';
import { formatLastMessage } from '../../lib/methods/formatLastMessage';
import useStatusAccessibilityLabel from '../../lib/hooks/useStatusAccessibilityLabel';
import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout';
import { CustomIcon } from '../CustomIcon';
import { useTheme } from '../../theme';

const RoomItem = ({
rid,
Expand Down Expand Up @@ -53,10 +55,12 @@ const RoomItem = ({
displayMode,
sourceType,
hideMentionStatus,
accessibilityDate
accessibilityDate,
isInvited
}: IRoomItemProps) => {
'use memo';

const { colors } = useTheme();
const { isLargeFontScale } = useResponsiveLayout();
const memoizedMessage = useMemo(
() => formatLastMessage({ lastMessage, username, useRealName, showLastMessage, alert, type }),
Expand Down Expand Up @@ -137,6 +141,15 @@ const RoomItem = ({
hideMentionStatus={hideMentionStatus}
hideUnreadStatus={hideUnreadStatus}
/>
{isInvited ? (
<CustomIcon
size={24}
name='mail'
role='status'
accessibilityLabel={I18n.t('Invited')}
color={colors.badgeBackgroundLevel2}
/>
) : null}
</View>
{isLargeFontScale ? <UpdatedAt date={date} hideUnreadStatus={hideUnreadStatus} alert={alert} /> : null}
</>
Expand Down
2 changes: 2 additions & 0 deletions app/containers/RoomItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isGroupChat } from '../../lib/methods/helpers';
import { formatDate, formatDateAccessibility } from '../../lib/methods/helpers/room';
import { type IRoomItemContainerProps } from './interfaces';
import RoomItem from './RoomItem';
import { isInviteSubscription } from '../../lib/methods/isInviteSubscription';

const attrs = ['width', 'isFocused', 'showLastMessage', 'autoJoin', 'showAvatar', 'displayMode'];

Expand Down Expand Up @@ -61,6 +62,7 @@ const RoomItemContainer = React.memo(
name={name}
avatar={avatar}
isGroupChat={isGroupChat(item)}
isInvited={isInviteSubscription(item)}
isRead={isRead}
onPress={handleOnPress}
onLongPress={handleOnLongPress}
Expand Down
1 change: 1 addition & 0 deletions app/containers/RoomItem/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface IRoomItemProps extends IBaseRoomItem {
testID: string;
status: TUserStatus;
isGroupChat: boolean;
isInvited?: boolean;
isRead: boolean;
teamMain: boolean;
date: string;
Expand Down
6 changes: 6 additions & 0 deletions app/definitions/ISubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ export interface ISubscription {
uploads: RelationModified<TUploadModel>;
disableNotifications?: boolean;
federated?: boolean;
inviter?: Required<Pick<IUser, '_id' | 'username'>> & Pick<IUser, 'name'>;
}

export interface IInviteSubscription extends ISubscription {
status: 'INVITED';
inviter: NonNullable<ISubscription['inviter']>;
}

export type TSubscriptionModel = ISubscription &
Expand Down
3 changes: 3 additions & 0 deletions app/definitions/rest/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export type RoomsEndpoints = {
success: boolean;
};
};
'rooms.invite': {
POST: (params: { roomId: string; action: 'accept' | 'reject' }) => void;
};
};

export type TRoomsMediaResponse = {
Expand Down
7 changes: 7 additions & 0 deletions app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@
"error-invalid-file-type": "Invalid file type",
"error-invalid-password": "Invalid password",
"error-invalid-room-name": "{{room_name}} is not a valid room name",
"error-invitation-reply-action": "Error while sending invitation reply",
"error-no-tokens-for-this-user": "There are no tokens for this user",
"error-not-allowed": "Not allowed",
"error-not-permission-to-upload-file": "You don't have permission to upload files",
Expand Down Expand Up @@ -416,6 +417,11 @@
"Invite_user_to_join_channel_all_from": "Invite all users from [#channel] to join this channel",
"Invite_user_to_join_channel_all_to": "Invite all users from this channel to join [#channel]",
"Invite_users": "Invite users",
"Invited": "Invited",
"invited_room_description_channel": "You've been invited by",
"invited_room_description_dm": "You've been invited to have a conversation with",
"invited_room_title_channel": "Invitation to join {{room_name}}",
"invited_room_title_dm": "Message request",
"IP": "IP",
"is_typing": "is typing",
"Italic": "Italic",
Expand Down Expand Up @@ -684,6 +690,7 @@
"Recording_audio_in_progress": "Recording audio message",
"Register": "Register",
"Registration_Succeeded": "Registration succeeded!",
"reject": "Reject",
"Remove": "Remove",
"remove": "remove",
"Remove_from_room": "Remove from room",
Expand Down
8 changes: 7 additions & 1 deletion app/lib/database/model/Subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export default class Subscription extends Model {

@field('federated') federated;

@field('status') status;

@json('inviter', sanitizer) inviter;

asPlain() {
return {
_id: this._id,
Expand Down Expand Up @@ -219,7 +223,9 @@ export default class Subscription extends Model {
usersCount: this.usersCount,
source: this.source,
disableNotifications: this.disableNotifications,
federated: this.federated
federated: this.federated,
status: this.status,
inviter: this.inviter
};
}
}
12 changes: 12 additions & 0 deletions app/lib/database/model/migrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,18 @@ export default schemaMigrations({
columns: [{ name: 'federated', type: 'boolean', isOptional: true }]
})
]
},
{
toVersion: 28,
steps: [
addColumns({
table: 'subscriptions',
columns: [
{ name: 'status', type: 'string', isOptional: true },
{ name: 'inviter', type: 'string', isOptional: true }
]
})
]
}
]
});
6 changes: 4 additions & 2 deletions app/lib/database/schema/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';

export default appSchema({
version: 27,
version: 28,
tables: [
tableSchema({
name: 'subscriptions',
Expand Down Expand Up @@ -70,7 +70,9 @@ export default appSchema({
{ name: 'users_count', type: 'number', isOptional: true },
{ name: 'unmuted', type: 'string', isOptional: true },
{ name: 'disable_notifications', type: 'boolean', isOptional: true },
{ name: 'federated', type: 'boolean', isOptional: true }
{ name: 'federated', type: 'boolean', isOptional: true },
{ name: 'status', type: 'string', isOptional: true },
{ name: 'inviter', type: 'string', isOptional: true }
]
}),
tableSchema({
Expand Down
21 changes: 21 additions & 0 deletions app/lib/methods/getInvitationData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type IInviteSubscription } from '../../definitions';
import I18n from '../../i18n';
import { getRoomTitle } from './helpers';
import { replyRoomInvite } from './replyRoomInvite';

export const getInvitationData = (room: IInviteSubscription) => {
const title =
room.t === 'd'
? I18n.t('invited_room_title_dm')
: I18n.t('invited_room_title_channel', { room_name: getRoomTitle(room).slice(0, 30) });

const description = room.t === 'd' ? I18n.t('invited_room_description_dm') : I18n.t('invited_room_description_channel');

return {
title,
description,
inviter: room.inviter,
accept: () => replyRoomInvite(room.id, 'accept'),
reject: () => replyRoomInvite(room.id, 'reject')
};
};
3 changes: 3 additions & 0 deletions app/lib/methods/helpers/mergeSubscriptionsRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../../definitions';
import { compareServerVersion } from './compareServerVersion';

// eslint-disable-next-line complexity
export const merge = (
subscription: ISubscription | IServerSubscription,
room?: IRoom | IServerRoom | IOmnichannelRoom
Expand Down Expand Up @@ -112,6 +113,8 @@ export const merge = (
mergedSubscription.autoTranslate = false;
}

mergedSubscription.status = mergedSubscription.status ?? undefined;
mergedSubscription.inviter = mergedSubscription.inviter ?? undefined;
mergedSubscription.blocker = !!mergedSubscription.blocker;
mergedSubscription.blocked = !!mergedSubscription.blocked;
mergedSubscription.hideMentionStatus = !!mergedSubscription.hideMentionStatus;
Expand Down
4 changes: 4 additions & 0 deletions app/lib/methods/isInviteSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type IInviteSubscription, type ISubscription } from '../../definitions';

export const isInviteSubscription = (subscription: ISubscription): subscription is IInviteSubscription =>
subscription?.status === 'INVITED' && !!subscription.inviter;
13 changes: 13 additions & 0 deletions app/lib/methods/replyRoomInvite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import i18n from '../../i18n';
import { sendInvitationReply } from '../services/restApi';
import { showErrorAlert } from './helpers';
import log from './helpers/log';

export const replyRoomInvite = async (rid: string, action: 'accept' | 'reject') => {
try {
await sendInvitationReply(rid, action);
} catch (e) {
showErrorAlert(i18n.t('error-invitation-reply-action'));
log(e);
}
};
2 changes: 2 additions & 0 deletions app/lib/services/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,8 @@ export function getUserInfo(userId: string) {

export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });

export const sendInvitationReply = (roomId: string, action: 'accept' | 'reject') => sdk.post('rooms.invite', { roomId, action });

export const videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean) =>
sdk.post('video-conference.join', { callId, state: { cam: !!cam, mic: mic === undefined ? true : mic } });

Expand Down
4 changes: 4 additions & 0 deletions app/views/RoomView/RightButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
return null;
}

if (status === 'INVITED') {
return null;
}

if (t === 'l') {
if (!this.isOmnichannelPreview()) {
return (
Expand Down
100 changes: 100 additions & 0 deletions app/views/RoomView/components/InvitedRoom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { type ReactElement } from 'react';
import { StyleSheet, Text, View } from 'react-native';

import { useTheme } from '../../../theme';
import { CustomIcon } from '../../../containers/CustomIcon';
import Button from '../../../containers/Button';
import sharedStyles from '../../Styles';
import I18n from '../../../i18n';
import type { IInviteSubscription } from '../../../definitions';
import Chip from '../../../containers/Chip';

const GAP = 32;

type InvitedRoomProps = {
title: string;
description: string;
inviter: IInviteSubscription['inviter'];
loading?: boolean;
onAccept: () => Promise<void>;
onReject: () => Promise<void>;
};

export const InvitedRoom = ({ title, description, inviter, loading, onAccept, onReject }: InvitedRoomProps): ReactElement => {
const { colors } = useTheme();
const styles = useStyle();

return (
<View style={styles.root}>
<View style={styles.container}>
<View style={styles.textView}>
<View style={styles.icon}>
<CustomIcon name='mail' size={42} color={colors.fontSecondaryInfo} />
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
<View style={styles.username}>
<Chip avatar={inviter.username} text={inviter.name || inviter.username} />
</View>
</View>
<Button title={I18n.t('accept')} loading={loading} onPress={onAccept} />
<Button
title={I18n.t('reject')}
type='secondary'
loading={loading}
backgroundColor={colors.surfaceTint}
onPress={onReject}
/>
</View>
</View>
);
};
Comment on lines 23 to 51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Loading state is not wired up in the parent component.

The loading prop exists but is never passed from RoomView/index.tsx (line 1586). Users won't see feedback while accept/reject actions are in progress. Consider either:

  1. Managing loading state in the parent and passing it down
  2. Managing loading state internally in this component
🔎 Example: internal loading state
-export const InvitedRoom = ({ title, description, inviter, loading, onAccept, onReject }: InvitedRoomProps): ReactElement => {
+export const InvitedRoom = ({ title, description, inviter, onAccept, onReject }: InvitedRoomProps): ReactElement => {
 	const { colors } = useTheme();
 	const styles = useStyle();
+	const [loading, setLoading] = React.useState(false);
+
+	const handleAccept = async () => {
+		setLoading(true);
+		try {
+			await onAccept();
+		} finally {
+			setLoading(false);
+		}
+	};
+
+	const handleReject = async () => {
+		setLoading(true);
+		try {
+			await onReject();
+		} finally {
+			setLoading(false);
+		}
+	};
 
 	return (
 		// ...
-			<Button title={I18n.t('accept')} loading={loading} onPress={onAccept} />
+			<Button title={I18n.t('accept')} loading={loading} onPress={handleAccept} />
 			<Button
 				title={I18n.t('reject')}
 				type='secondary'
 				loading={loading}
 				backgroundColor={colors.surfaceTint}
-				onPress={onReject}
+				onPress={handleReject}
 			/>
 		// ...
 	);
 };
🤖 Prompt for AI Agents
In app/views/RoomView/components/InvitedRoom.tsx around lines 23-51: the
component accepts a loading prop but the parent (RoomView/index.tsx at ~line
1586) never passes it, so users get no feedback during accept/reject operations;
fix by either A) add loading state in the parent and pass it down: create a
boolean state (e.g., isInvitedActionLoading), set it true before awaiting the
accept/reject async call and false afterwards (handle errors and finally block),
then pass loading={isInvitedActionLoading} into <InvitedRoom>, or B) manage
loading internally in InvitedRoom: add a local useState loading flag, wrap
onAccept/onReject handlers to setLoading(true) before awaiting the provided
callback and setLoading(false) in finally, and use that state for the Button
loading prop; ensure callbacks remain async-safe and errors are handled.


const useStyle = () => {
const { colors } = useTheme();
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.surfaceRoom
},
container: {
flex: 1,
marginHorizontal: 24,
justifyContent: 'center'
},
textView: { alignItems: 'center' },
icon: {
width: 58,
height: 58,
borderRadius: 30,
marginBottom: GAP,
backgroundColor: colors.surfaceNeutral,
alignItems: 'center',
justifyContent: 'center'
},
title: {
...sharedStyles.textBold,
fontSize: 24,
lineHeight: 32,
textAlign: 'center',
color: colors.fontTitlesLabels,
marginBottom: GAP
},
description: {
...sharedStyles.textRegular,
fontSize: 16,
lineHeight: 24,
textAlign: 'center',
color: colors.fontDefault
},
username: {
...sharedStyles.textRegular,
fontSize: 16,
lineHeight: 24,
textAlign: 'center',
color: colors.fontDefault,
marginBottom: GAP
}
});
return styles;
};
4 changes: 3 additions & 1 deletion app/views/RoomView/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@ export const roomAttrsUpdate = [
'autoTranslateLanguage',
'unmuted',
'E2EKey',
'encrypted'
'encrypted',
'status',
'inviter'
] as TRoomUpdate[];
Loading