diff --git a/app/lib/methods/getUsersPresence.ts b/app/lib/methods/getUsersPresence.ts index 798fe37ebec..063defadb7e 100644 --- a/app/lib/methods/getUsersPresence.ts +++ b/app/lib/methods/getUsersPresence.ts @@ -1,5 +1,6 @@ import { InteractionManager } from 'react-native'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; +import { Q } from '@nozbe/watermelondb'; import { type IActiveUsers } from '../../reducers/activeUsers'; import { store as reduxStore } from '../store/auxStore'; @@ -9,6 +10,7 @@ import database from '../database'; import { type IUser } from '../../definitions'; import sdk from '../services/sdk'; import { compareServerVersion } from './helpers'; +import log from './helpers/log'; import userPreferences from './userPreferences'; import { NOTIFICATION_PRESENCE_CAP } from '../constants/notifications'; import { setNotificationPresenceCap } from '../../actions/app'; @@ -140,3 +142,31 @@ export const setPresenceCap = async (enabled: boolean) => { reduxStore.dispatch(setNotificationPresenceCap(false)); } }; + +export const getDirectMessageUserIds = async (): Promise => { + try { + const db = database.active; + const loggedUserId = reduxStore.getState().login.user?.id; + const subscriptionsCollection = db.get('subscriptions'); + const subscriptions = await subscriptionsCollection + .query(Q.where('t', 'd'), Q.where('open', true), Q.where('archived', false)) + .fetch(); + const userIds = subscriptions + .map((sub: { uids?: string[] }) => sub.uids?.find(uid => uid && uid !== loggedUserId)) + .filter((uid): uid is string => Boolean(uid)); + return [...new Set(userIds)]; + } catch (e) { + return []; + } +}; + +export const refreshDmUsersPresence = async (): Promise => { + try { + const dmUserIds = await getDirectMessageUserIds(); + if (dmUserIds.length > 0) { + await getUsersPresence(dmUserIds); + } + } catch (e) { + log(e); + } +}; diff --git a/app/sagas/login.js b/app/sagas/login.js index e88740ed6ff..d5ab793575c 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -28,7 +28,7 @@ import { getEnterpriseModules, isOmnichannelModuleAvailable } from '../lib/metho import { getPermissions } from '../lib/methods/getPermissions'; import { getRoles } from '../lib/methods/getRoles'; import { getSlashCommands } from '../lib/methods/getSlashCommands'; -import { getUserPresence, subscribeUsersPresence } from '../lib/methods/getUsersPresence'; +import { getUserPresence, subscribeUsersPresence, refreshDmUsersPresence } from '../lib/methods/getUsersPresence'; import { logout, removeServerData, removeServerDatabase } from '../lib/methods/logout'; import { subscribeSettings } from '../lib/methods/getSettings'; import { connect, loginWithPassword, login } from '../lib/services/connect'; @@ -200,6 +200,14 @@ const fetchUsersPresenceFork = function* fetchUsersPresenceFork() { } }; +const fetchDmUsersPresenceFork = function* fetchDmUsersPresenceFork() { + try { + yield call(refreshDmUsersPresence); + } catch (e) { + log('[login.js] Error fetching DM users presence:', e); + } +}; + const fetchEnterpriseModulesFork = function* fetchEnterpriseModulesFork({ user }) { try { yield getEnterpriseModules(); @@ -253,6 +261,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { yield fork(fetchSlashCommandsFork); yield fork(registerPushTokenFork); yield fork(fetchUsersPresenceFork); + yield fork(fetchDmUsersPresenceFork); yield fork(fetchEnterpriseModulesFork, { user }); yield fork(subscribeSettingsFork); yield fork(fetchUsersRoles); diff --git a/app/sagas/state.js b/app/sagas/state.js index 20272cfeb73..edf9dc8e50d 100644 --- a/app/sagas/state.js +++ b/app/sagas/state.js @@ -1,12 +1,16 @@ -import { select, takeLatest } from 'redux-saga/effects'; +import { call, delay, select, takeLatest } from 'redux-saga/effects'; import log from '../lib/methods/helpers/log'; import { localAuthenticate, saveLastLocalAuthenticationSession } from '../lib/methods/helpers/localAuthentication'; -import { APP_STATE } from '../actions/actionsTypes'; +import { APP_STATE, METEOR } from '../actions/actionsTypes'; import { RootEnum } from '../definitions'; import { checkAndReopen } from '../lib/services/connect'; import { setUserPresenceOnline, setUserPresenceAway } from '../lib/services/restApi'; import { checkPendingNotification } from '../lib/notifications'; +import { refreshDmUsersPresence } from '../lib/methods/getUsersPresence'; + +const CONNECTION_RETRY_LIMIT = 10; +const CONNECTION_RETRY_DELAY_MS = 1000; const isAuthAndConnected = function* isAuthAndConnected() { const login = yield select(state => state.login); @@ -14,24 +18,37 @@ const isAuthAndConnected = function* isAuthAndConnected() { return login.isAuthenticated && meteor.connected; }; -const appHasComeBackToForeground = function* appHasComeBackToForeground() { - const appRoot = yield select(state => state.app.root); - if (appRoot !== RootEnum.ROOT_INSIDE) { - return; - } - const isReady = yield isAuthAndConnected(); - if (!isReady) { - return; +const waitForConnection = function* waitForConnection() { + let retries = 0; + let isReady = yield isAuthAndConnected(); + while (!isReady && retries < CONNECTION_RETRY_LIMIT) { + yield delay(CONNECTION_RETRY_DELAY_MS); + isReady = yield isAuthAndConnected(); + retries++; } + return isReady; +}; + +const appHasComeBackToForeground = function* appHasComeBackToForeground() { try { const server = yield select(state => state.server.server); yield localAuthenticate(server); checkAndReopen(); + + const isReady = yield waitForConnection(); + if (!isReady) { + log('[state.js] Connection not ready after retries, aborting foreground tasks'); + return; + } + + // Refresh presence for DM users to ensure status is up-to-date after background + yield setUserPresenceOnline(); + yield call(refreshDmUsersPresence); + // Check for pending notification when app comes to foreground (Android - notification tap while in background) checkPendingNotification().catch(e => { log('[state.js] Error checking pending notification:', e); }); - return yield setUserPresenceOnline(); } catch (e) { log(e); } @@ -55,9 +72,22 @@ const appHasComeBackToBackground = function* appHasComeBackToBackground() { } }; +const handleMeteorConnect = function* handleMeteorConnect() { + const appRoot = yield select(state => state.app.root); + if (appRoot !== RootEnum.ROOT_INSIDE) { + return; + } + try { + yield call(refreshDmUsersPresence); + } catch (e) { + log('[state.js] Error refreshing DM users presence on connect:', e); + } +}; + const root = function* root() { yield takeLatest(APP_STATE.FOREGROUND, appHasComeBackToForeground); yield takeLatest(APP_STATE.BACKGROUND, appHasComeBackToBackground); + yield takeLatest(METEOR.SUCCESS, handleMeteorConnect); }; export default root;