Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6def48c
chore: update @rocket.chat/sdk to mobile branch HEAD
diegolmello Aug 14, 2026
34e6d0f
test: integration-test the app against the real SDK lib
diegolmello Aug 15, 2026
37925d9
chore: bump @rocket.chat/sdk to mobile HEAD 4202408
diegolmello Aug 17, 2026
e32cca6
chore: bump @rocket.chat/sdk to mobile HEAD and drop local module shims
diegolmello Aug 18, 2026
fa7d715
fix: address review — await voip listener, filter undefined upload he…
diegolmello Aug 18, 2026
26c208c
Merge branch 'develop' into new-sdk
diegolmello Aug 18, 2026
7b8f589
chore: bump @rocket.chat/sdk to mobile HEAD 383e457b and drop sdk cli…
diegolmello Aug 18, 2026
574125b
fix: remove unreachable ping-age branch in classifySocketHealth
diegolmello Aug 19, 2026
736de5e
fix(login): surface a failure instead of hanging on a missing login r…
diegolmello Aug 19, 2026
341470f
fix(upload): refuse an upload without auth headers
diegolmello Aug 19, 2026
4547349
fix(2fa): report a cancelled two-factor prompt as a cancellation
diegolmello Aug 19, 2026
8ee07d5
fix(2fa): stop reporting a deliberate cancellation as an error
diegolmello Aug 19, 2026
81f32c6
fix: settle the promises that could strand a caller
diegolmello Aug 19, 2026
ef8549e
chore: drop a comment describing the removed empty two-factor response
diegolmello Aug 19, 2026
902cb1e
fix(2fa): cover the cancellation paths that bypass central suppression
diegolmello Aug 19, 2026
abcf41d
fix(upload): validate auth headers when the upload is sent
diegolmello Aug 19, 2026
654e8c0
fix(2fa): stop alerting when the avatar prompt is cancelled
diegolmello Aug 19, 2026
b47c776
chore: adopt the SDK's login types and test against the real lib (#7582)
diegolmello Aug 20, 2026
155f4ea
chore: update @rocket.chat/sdk to mobile HEAD (#7583)
diegolmello Aug 20, 2026
6d3cbd5
chore: narrow subscribeSettings and drop its unused SDK type import (…
diegolmello Aug 20, 2026
e5e5929
test: use the app's own TDriver instead of reaching into SDK internal…
diegolmello Aug 20, 2026
57409d7
chore: remove lint suppressions for a disabled rule (#7586)
diegolmello Aug 20, 2026
d37a350
Merge remote-tracking branch 'origin/develop' into new-sdk
diegolmello Aug 21, 2026
f9930e8
chore: bump @rocket.chat/sdk to mobile HEAD b6453cc3
diegolmello Aug 21, 2026
dd5da3b
test: cover multi-workspace switching (#7590)
diegolmello Aug 21, 2026
ac50470
fix: server version recorded as undefined when falling back to anothe…
diegolmello Aug 21, 2026
6a4e293
fix(logout): keep other workspaces reachable after a forced logout (#…
diegolmello Aug 21, 2026
c2e0de3
fix: give every saga exit a user-facing root (#7592)
diegolmello Aug 21, 2026
0f2a862
test: cover the background/foreground socket resume path (#7589)
diegolmello Aug 21, 2026
5094c14
fix: make sdk.current nullable and guard its call sites (#7587)
diegolmello Aug 24, 2026
778fa86
test: cover messages received while the device is offline (#7596)
diegolmello Aug 24, 2026
ff9d54e
chore: drop comments that restate or narrate code
diegolmello Aug 24, 2026
150fd8c
refactor: share the logged-in server lookup between the init and logi…
diegolmello Aug 24, 2026
0e547f2
chore: drop the outcome enumeration from the recovery docblock
diegolmello Aug 24, 2026
bf54234
refactor: type the logged-in server lookup with TServerModel
diegolmello Aug 24, 2026
e37fd25
fix: use the room _id when navigating to a newly created direct message
diegolmello Aug 24, 2026
8746684
Merge branch 'develop' into new-sdk
diegolmello Aug 24, 2026
8382470
chore: bump @rocket.chat/sdk to latest mobile HEAD
diegolmello Aug 24, 2026
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
4 changes: 2 additions & 2 deletions .github/scripts/__tests__/fixtures/scenario-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@
},
{
"id": "C3",
"name": "shared saga fans wide: sagas/rooms.js -> twelve flows",
"name": "shared saga fans wide: sagas/rooms.js -> thirteen flows",
"category": "real-domain",
"input": { "diff": ["app/sagas/rooms.js"] },
"expectedShards": [1, 5, 6, 7, 8, 11, 12, 13, 14],
"expectedShards": [1, 3, 5, 6, 7, 8, 11, 12, 13, 14],
"expectedShouldRun": true,
"assertableIn": ["map", "ci"]
},
Expand Down
3 changes: 2 additions & 1 deletion .maestro/scripts/data-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,5 +263,6 @@ output.utils = {
post,
login,
getDeepLink,
createDM
createDM,
sleep
};
55 changes: 55 additions & 0 deletions .maestro/tests/room/messages-received-while-offline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
appId: ${APP_ID}
name: Messages received while device is offline
onFlowStart:
- runFlow: '../../helpers/setup.yaml'
onFlowComplete:
- setAirplaneMode: disabled
- stopApp: ${APP_ID}
- evalScript: ${output.utils.deleteCreatedUsers()}
tags:
- test-3
- android-only

---
- evalScript: ${output.user = output.utils.createUser()}
- evalScript: ${output.sender = output.utils.createUser()}
- evalScript: ${output.tag = 'offline-' + output.random(6)}
- evalScript: ${output.utils.sendMessage(output.sender.username, output.sender.password, '@' + output.user.username, output.tag + '-baseline')}

- runFlow:
file: '../../helpers/login-with-deeplink.yaml'
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
CLEAR_STATE: true
- runFlow:
file: '../../helpers/navigate-to-room.yaml'
env:
ROOM: ${output.sender.username}
- extendedWaitUntil:
visible:
id: 'message-content-${output.tag}-baseline'
timeout: 60000

# should deliver every message that arrived while the device had no network
- setAirplaneMode: enabled
- evalScript: ${output.utils.sendMessage(output.sender.username, output.sender.password, '@' + output.user.username, output.tag + '-1')}
- evalScript: ${output.utils.sendMessage(output.sender.username, output.sender.password, '@' + output.user.username, output.tag + '-2')}
- evalScript: ${output.utils.sendMessage(output.sender.username, output.sender.password, '@' + output.user.username, output.tag + '-3')}
- evalScript: ${output.utils.sleep(5000)}
- assertNotVisible:
id: 'message-content-${output.tag}-1'
- assertNotVisible:
id: 'message-content-${output.tag}-2'
- assertNotVisible:
id: 'message-content-${output.tag}-3'
- setAirplaneMode: disabled

- extendedWaitUntil:
visible:
id: 'message-content-${output.tag}-3'
timeout: 120000
- assertVisible:
id: 'message-content-${output.tag}-1'
- assertVisible:
id: 'message-content-${output.tag}-2'
20 changes: 18 additions & 2 deletions .sniffler/test-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"app/views/RegisterView/**",
"app/views/RoomsListView/**",
"app/sagas/login.js",
"app/sagas/rooms.js"
"app/sagas/rooms.js",
"app/sagas/selectServer.ts",
"app/lib/services/connect.ts"
]
},
{
Expand All @@ -66,7 +68,9 @@
"app/views/RegisterView/**",
"app/views/RoomsListView/**",
"app/sagas/login.js",
"app/sagas/rooms.js"
"app/sagas/rooms.js",
"app/sagas/selectServer.ts",
"app/lib/services/connect.ts"
]
},
{
Expand Down Expand Up @@ -249,6 +253,18 @@
"test": ".maestro/tests/room/mark-as-unread.yaml",
"dependsOn": ["app/views/RoomsListView/**", "app/containers/MessageActions/**", "app/sagas/rooms.js"]
},
{
"test": ".maestro/tests/room/messages-received-while-offline.yaml",
"dependsOn": [
"app/lib/services/sdk.ts",
"app/lib/services/socketHealth.ts",
"app/lib/methods/loadMissedMessages.ts",
"app/lib/methods/subscriptions/room.ts",
"app/lib/methods/subscribeRooms.ts",
"app/sagas/rooms.js",
"app/views/RoomView/**"
]
},
{
"test": ".maestro/tests/room/message-markdown-click.yaml",
"dependsOn": ["app/views/RoomView/**", "app/containers/markdown/**", "app/sagas/room.js"]
Expand Down
37 changes: 19 additions & 18 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

## Rooms & Conversations

| Term | Definition | Aliases to avoid |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| **Room** | A server-side conversation container with shared state (name, type, settings) | Chat, conversation |
| **Subscription** | A user's personal relationship to a Room, holding per-user state (unread count, favorite, muted, open) | Membership, room entry |
| **Channel** | A public Room (type `'c'`) visible to all server users | Public room |
| **Group** | A private Room (type `'p'`) visible only to invited members | Private room, private channel |
| **Direct Message** | A 1-on-1 private Room (type `'d'`) between two users | DM, PM, private message |
| **Thread** | A branched conversation spawned from a single Message, identified by `tmid` (thread message id) | Reply chain |
| **Discussion** | A separate Room spawned from a parent Room, identified by `prid` (parent room id) — unlike Threads, Discussions are full Rooms | Sub-room, sub-channel |
| **Team** | An organizational container that groups multiple Channels and users under a single entity | Workspace (ambiguous) |
| **Broadcast Room** | A Room where only authorized users can send Messages; other users can only Reply Broadcast to existing Messages | Broadcast channel |
| **Reply Broadcast** | The action of replying to a Message in a Broadcast Room when the current user cannot send regular Messages | Broadcast reply |
| Term | Definition | Aliases to avoid |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **Room** | A server-side conversation container with shared state (name, type, settings) | Chat, conversation |
| **Subscription** | A user's personal relationship to a Room, holding per-user state (unread count, favorite, muted, open) — never a **DDP Subscription** | Membership, room entry |
| **Channel** | A public Room (type `'c'`) visible to all server users | Public room |
| **Group** | A private Room (type `'p'`) visible only to invited members | Private room, private channel |
| **Direct Message** | A 1-on-1 private Room (type `'d'`) between two users | DM, PM, private message |
| **Thread** | A branched conversation spawned from a single Message, identified by `tmid` (thread message id) | Reply chain |
| **Discussion** | A separate Room spawned from a parent Room, identified by `prid` (parent room id) — unlike Threads, Discussions are full Rooms | Sub-room, sub-channel |
| **Team** | An organizational container that groups multiple Channels and users under a single entity | Workspace (ambiguous) |
| **Broadcast Room** | A Room where only authorized users can send Messages; other users can only Reply Broadcast to existing Messages | Broadcast channel |
| **Reply Broadcast** | The action of replying to a Message in a Broadcast Room when the current user cannot send regular Messages | Broadcast reply |

## Messages

Expand Down Expand Up @@ -197,12 +197,13 @@ A **Message Action** is the active mode on a Message in the Room view. The three

## Server & Connection

| Term | Definition | Aliases to avoid |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| **Server** | A Rocket.Chat server instance the app connects to, with version, settings, and enterprise modules | Workspace (used by web but not consistently in mobile), instance |
| **Server History** | List of previously connected Servers for quick reconnection | Recent servers |
| **Meteor Connect** | The WebSocket connection to the Server's DDP (Distributed Data Protocol) endpoint | Socket, connection |
| **Socket Health** | Whether the Meteor Connect socket is genuinely alive — confirmed by a round trip when in doubt, reopened when known dead | Staleness (stale/gray/fresh), socket probe |
| Term | Definition | Aliases to avoid |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| **Server** | A Rocket.Chat server instance the app connects to, with version, settings, and enterprise modules | Workspace (used by web but not consistently in mobile), instance |
| **Server History** | List of previously connected Servers for quick reconnection | Recent servers |
| **Meteor Connect** | The WebSocket connection to the Server's DDP (Distributed Data Protocol) endpoint | Socket, connection |
| **Socket Health** | Whether the Meteor Connect socket is genuinely alive — confirmed by a round trip when in doubt, reopened when known dead | Staleness (stale/gray/fresh), socket probe |
| **DDP Subscription** | A live server-push feed on Meteor Connect, opened by name and parameters (`stream-room-messages`, `stream-notify-user`); the SDK derives its id from those parameters, so two callers asking for the same feed share one — distinct from a **Subscription**, which is a membership record | Stream, DDP stream, sub |

## Navigation & Layout

Expand Down
4 changes: 2 additions & 2 deletions app/containers/Avatar/useAvatarETag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const useAvatarETag = ({
id
}: {
type?: string;
username: string;
username?: string;
Comment thread
diegolmello marked this conversation as resolved.
text: string;
rid?: string;
id: string;
Expand Down Expand Up @@ -61,7 +61,7 @@ export const useAvatarETag = ({
}
};
}
}, [text]);
}, [text, username, type, rid, id]);

return { avatarETag };
};
6 changes: 5 additions & 1 deletion app/containers/LoginServices/serviceLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ export const onPressAppleLogin = async () => {
AppleAuthentication.AppleAuthenticationScope.EMAIL
]
});
await loginOAuthOrSso({ fullName, email, identityToken });
if (!identityToken) {
logEvent(events.ENTER_WITH_APPLE_F);
return;
}
await loginOAuthOrSso({ fullName: fullName ?? {}, email, identityToken });
} catch {
logEvent(events.ENTER_WITH_APPLE_F);
}
Expand Down
38 changes: 38 additions & 0 deletions app/containers/TwoFactor/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';

import TwoFactor from '.';
import { isTwoFactorCancelled, twoFactor } from '../../lib/services/twoFactor';

jest.mock('../../lib/services/restApi', () => ({
sendEmailCode: jest.fn()
}));

jest.mock('../../lib/hooks/useMasterDetail', () => ({
useMasterDetail: () => false
}));

const requestTwoFactor = () => twoFactor({ method: 'totp', invalid: false });

describe('TwoFactor', () => {
it('cancels the displaced prompt and resolves the newest one', async () => {
const { getByTestId } = render(<TwoFactor />);

let displacedResult: Promise<unknown> | undefined;
let newest: ReturnType<typeof requestTwoFactor> | undefined;
await act(() => {
displacedResult = requestTwoFactor().catch(error => error);
newest = requestTwoFactor();
});

await waitFor(() => expect(getByTestId('two-factor-input')).toBeTruthy());

expect(isTwoFactorCancelled(await displacedResult!)).toBe(true);

fireEvent.changeText(getByTestId('two-factor-input'), '123456');
await act(() => {
fireEvent.press(getByTestId('two-factor-send'));
});

await expect(newest!).resolves.toEqual({ twoFactorCode: '123456', twoFactorMethod: 'totp' });
});
});
16 changes: 11 additions & 5 deletions app/containers/TwoFactor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState, memo } from 'react';
import { useEffect, useRef, useState, memo } from 'react';
import { AccessibilityInfo, Text, View } from 'react-native';
import isEmpty from 'lodash/isEmpty';
import { sha256 } from 'js-sha256';
Expand All @@ -16,11 +16,11 @@
import Button from '../Button';
import sharedStyles from '../../views/Styles';
import styles from './styles';
import { type ICredentials } from '../../definitions';
import { type ILoginCredentials } from '../../definitions';
import { sendEmailCode } from '../../lib/services/restApi';

Check warning on line 20 in app/containers/TwoFactor/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint and Test / run-eslint-and-test

import(no-cycle)

Dependency cycle detected
import { useMasterDetail } from '../../lib/hooks/useMasterDetail';
import Toast from '../Toast';
import { showToast } from '../../lib/methods/helpers/showToast';

Check warning on line 23 in app/containers/TwoFactor/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint and Test / run-eslint-and-test

import(no-cycle)

Dependency cycle detected
import log from '../../lib/methods/helpers/log';

export const TWO_FACTOR = 'TWO_FACTOR';
Expand All @@ -38,7 +38,7 @@
}

interface EventListenerMethod {
params?: ICredentials;
params?: ILoginCredentials;
method?: keyof IMethods;
submit?: (param: string) => void;
cancel?: () => void;
Expand Down Expand Up @@ -70,6 +70,7 @@
const isMasterDetail = useMasterDetail();
const [visible, setVisible] = useState(false);
const [data, setData] = useState<EventListenerMethod>({});
const pendingCancel = useRef<EventListenerMethod['cancel']>(undefined);
const {
control,
setValue,
Expand All @@ -87,12 +88,13 @@
const method = data.method ? methods[data.method] : null;
const isEmail = data.method === 'email';
const params = data?.params;
const emailCodeRecipient = params && 'user' in params ? params.user : undefined;

const sendEmail = async () => {
try {
if (params?.user) {
if (emailCodeRecipient) {
clearErrors();
const response = await sendEmailCode(params?.user);
const response = await sendEmailCode(emailCodeRecipient);

if (response.success) {
showToast(I18n.t('Two_Factor_Success_message'));
Expand All @@ -113,6 +115,8 @@
}, [data]);

const showTwoFactor = (args: EventListenerMethod) => {
pendingCancel.current?.();
pendingCancel.current = args.cancel;
setData(args);
if (args.invalid) {
setError('code', { message: I18n.t('Invalid_code'), type: 'validate' });
Expand All @@ -128,6 +132,7 @@

const onCancel = () => {
const { cancel } = data;
pendingCancel.current = undefined;
if (cancel) {
cancel();
}
Expand All @@ -136,6 +141,7 @@

const onSubmit = () => {
const { submit } = data;
pendingCancel.current = undefined;
if (submit) {
const { code } = getValues();
if (data.method === 'password') {
Expand Down
24 changes: 0 additions & 24 deletions app/definitions/ICredentials.ts

This file was deleted.

18 changes: 2 additions & 16 deletions app/definitions/ILoggedUser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type Model from '@nozbe/watermelondb/Model';

import { type IUserEmail, type IUserSettings } from './IUser';
import { type IUserEmail } from './IUser';
import { type TStatusSource } from './TStatusSource';
import { type TUserStatus } from './TUserStatus';

export interface ILoggedUser {
id: string;
token: string;
username: string;
username?: string;
name?: string;
language?: string;
status: TUserStatus;
Expand All @@ -30,18 +30,4 @@ export interface ILoggedUser {
requirePasswordChange?: boolean;
}

export interface ILoggedUserResultFromServer extends Omit<
ILoggedUser,
'enableMessageParserEarlyAdoption' | 'showMessageInMainThread'
> {
settings: IUserSettings;
}

export interface ILoginResultFromServer {
status: string;
authToken: string;
userId: string;
me: ILoggedUserResultFromServer;
}

export type TLoggedUserModel = ILoggedUser & Model;
12 changes: 12 additions & 0 deletions app/definitions/ILoginCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type {
ICredentialsAppleAPI,
ICredentialsAuthenticated,
ICredentialsCasAPI,
ICredentialsCrowdAPI,
ICredentialsLdapAPI,
ICredentialsOAuth,
ICredentialsPasswordAPI,
ICredentialsSamlAPI,
ICredentialsTotpAPI,
ILoginCredentials
} from '@rocket.chat/sdk/interfaces';
2 changes: 1 addition & 1 deletion app/definitions/IProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type ReactNode } from 'react';
export interface IProfileParams {
realname?: string;
name?: string;
username: string;
username?: string;
email: string | null;
newPassword: string;
currentPassword: string;
Expand Down
Loading
Loading