diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts index bb98cd5db..c258ba8a3 100644 --- a/app/actions/app/global.ts +++ b/app/actions/app/global.ts @@ -77,3 +77,11 @@ export const removeLastViewedChannelIdAndServer = async () => { export const removeLastViewedThreadIdAndServer = async () => { return storeGlobal(GLOBAL_IDENTIFIERS.LAST_VIEWED_THREAD, null, false); }; + +export const storePushDisabledInServerAcknowledged = async (serverUrl: string) => { + return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, 'true', false); +}; + +export const removePushDisabledInServerAcknowledged = async (serverUrl: string) => { + return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, null, false); +}; diff --git a/app/constants/database.ts b/app/constants/database.ts index f14d22a62..d302c280a 100644 --- a/app/constants/database.ts +++ b/app/constants/database.ts @@ -82,6 +82,7 @@ export const GLOBAL_IDENTIFIERS = { ONBOARDING: 'onboarding', LAST_VIEWED_CHANNEL: 'lastViewedChannel', LAST_VIEWED_THREAD: 'lastViewedThread', + PUSH_DISABLED_ACK: 'pushDisabledAck', }; export enum OperationType { diff --git a/app/managers/session_manager.ts b/app/managers/session_manager.ts index 0e21ca4a4..c79a69cff 100644 --- a/app/managers/session_manager.ts +++ b/app/managers/session_manager.ts @@ -5,7 +5,7 @@ import CookieManager, {type Cookie} from '@react-native-cookies/cookies'; import {AppState, type AppStateStatus, DeviceEventEmitter, Platform} from 'react-native'; import FastImage from 'react-native-fast-image'; -import {storeOnboardingViewedValue} from '@actions/app/global'; +import {removePushDisabledInServerAcknowledged, storeOnboardingViewedValue} from '@actions/app/global'; import {cancelSessionNotification, logout, scheduleSessionNotification} from '@actions/remote/session'; import {Events, Launch} from '@constants'; import DatabaseManager from '@database/manager'; @@ -21,7 +21,7 @@ import {getCurrentUser} from '@queries/servers/user'; import {getThemeFromState} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import {deleteFileCache, deleteFileCacheByDir} from '@utils/file'; -import {isMainActivity} from '@utils/helpers'; +import {extractCleanDomain, isMainActivity} from '@utils/helpers'; import {addNewServer} from '@utils/server'; import type {LaunchType} from '@typings/launch'; @@ -168,6 +168,8 @@ class SessionManager { await this.terminateSession(serverUrl, removeServer); + await removePushDisabledInServerAcknowledged(extractCleanDomain(serverUrl)); + if (activeServerUrl === serverUrl) { let displayName = ''; let launchType: LaunchType = Launch.AddServer; diff --git a/app/queries/app/global.ts b/app/queries/app/global.ts index 9784c6124..e1ff99bb8 100644 --- a/app/queries/app/global.ts +++ b/app/queries/app/global.ts @@ -55,6 +55,22 @@ export const getDontAskForReview = async () => { return Boolean(records?.[0]?.value); }; +export const getPushDisabledInServerAcknowledged = async (serverDomainString: string) => { + const records = await queryGlobalValue(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverDomainString}`)?.fetch(); + return Boolean(records?.[0]?.value); +}; + +export const observePushDisabledInServerAcknowledged = (serverDomainString: string) => { + const query = queryGlobalValue(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverDomainString}`); + if (!query) { + return of$(false); + } + return query.observe().pipe( + switchMap((result) => (result.length ? result[0].observe() : of$(false))), + switchMap((v) => of$(Boolean(v))), + ); +}; + export const getFirstLaunch = async () => { const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.FIRST_LAUNCH)?.fetch(); if (!records?.[0]?.value) { diff --git a/app/screens/home/channel_list/categories_list/header/header.test.tsx b/app/screens/home/channel_list/categories_list/header/header.test.tsx index 2832e7f3d..af40f111a 100644 --- a/app/screens/home/channel_list/categories_list/header/header.test.tsx +++ b/app/screens/home/channel_list/categories_list/header/header.test.tsx @@ -17,6 +17,7 @@ describe('components/channel_list/header', () => { canJoinChannels={true} canInvitePeople={true} displayName={'Test!'} + pushDisabledAck={true} />, ); diff --git a/app/screens/home/channel_list/categories_list/header/header.tsx b/app/screens/home/channel_list/categories_list/header/header.tsx index 4cec3fde3..fb1528d9b 100644 --- a/app/screens/home/channel_list/categories_list/header/header.tsx +++ b/app/screens/home/channel_list/categories_list/header/header.tsx @@ -38,6 +38,7 @@ type Props = { iconPad?: boolean; onHeaderPress?: () => void; pushProxyStatus: string; + pushDisabledAck: boolean; } const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({ @@ -111,6 +112,7 @@ const ChannelListHeader = ({ iconPad, onHeaderPress, pushProxyStatus, + pushDisabledAck, }: Props) => { const theme = useTheme(); const isTablet = useIsTablet(); @@ -166,7 +168,7 @@ const ChannelListHeader = ({ const onPushAlertPress = useCallback(() => { if (pushProxyStatus === PUSH_PROXY_STATUS_NOT_AVAILABLE) { - alertPushProxyError(intl); + alertPushProxyError(intl, serverUrl); } else { alertPushProxyUnknown(intl); } @@ -206,7 +208,7 @@ const ChannelListHeader = ({ > {serverDisplayName} - {(pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED) && ( + {pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && !pushDisabledAck && ( { +type Props = WithDatabaseArgs & { + serverUrl: string; +} + +const enhanced = withObservables([], ({serverUrl, database}: Props) => { const team = observeCurrentTeam(database); const currentUser = observeCurrentUser(database); @@ -57,7 +64,8 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { distinctUntilChanged(), ), pushProxyStatus: observePushVerificationStatus(database), + pushDisabledAck: observePushDisabledInServerAcknowledged(extractCleanDomain(serverUrl)), }; }); -export default withDatabase(enhanced(ChannelListHeader)); +export default withDatabase(withServerUrl(enhanced(ChannelListHeader))); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts index a46828e71..626f5121d 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts +++ b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts @@ -4,10 +4,11 @@ import withObservables from '@nozbe/with-observables'; import {of as of$} from 'rxjs'; +import {extractCleanDomain} from '@app/utils/helpers'; import {Tutorial} from '@constants'; import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy'; import DatabaseManager from '@database/manager'; -import {observeTutorialWatched} from '@queries/app/global'; +import {observePushDisabledInServerAcknowledged, observeTutorialWatched} from '@queries/app/global'; import {observePushVerificationStatus} from '@queries/servers/system'; import ServerItem from './server_item'; @@ -26,6 +27,7 @@ const enhance = withObservables(['highlight'], ({highlight, server}: {highlight: server: server.observe(), tutorialWatched, pushProxyStatus: serverDatabase ? observePushVerificationStatus(serverDatabase) : of$(PUSH_PROXY_STATUS_UNKNOWN), + pushDisabledAck: observePushDisabledInServerAcknowledged(extractCleanDomain(server.url)), }; }); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx index efe10083e..0f463cba6 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx @@ -44,6 +44,7 @@ type Props = { server: ServersModel; tutorialWatched: boolean; pushProxyStatus: string; + pushDisabledAck: boolean; } type BadgeValues = { @@ -147,6 +148,7 @@ const ServerItem = ({ server, tutorialWatched, pushProxyStatus, + pushDisabledAck, }: Props) => { const intl = useIntl(); const theme = useTheme(); @@ -428,7 +430,7 @@ const ServerItem = ({ > {displayName} - {server.lastActiveAt > 0 && pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && ( + {server.lastActiveAt > 0 && pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && !pushDisabledAck && ( Result: example-something + * // Input: http://192.168.1.1 => Result: 192.168.1.1 + * // Input: https://127.0.0.1:3000 => Result: 127.0.0.1 + */ +export function extractDomain(input: string) { + const regex = /^(https?:\/\/)?([\w.-]+)(:\d+)?$/; + const match = input.match(regex); + + if (match && match[2]) { + return match[2].replace(/[\W.]/g, ''); + } + + return ''; +} + +/** + * Returns the domain name or IP address from a serverUrl without special characters. + * + * @param {string} domain - The extracted domain from a serverUrl. + * @returns {string} The extracted domain name or IP address, modified to remove special characters, + * or empty . + * + * // Output: + * // Input: example-something => Result: examplesomething + * // Input: 192.168.1.1 => Result: 19216811 + * // Input: 127.0.0.1 => Result: 127001 + */ +export function extractCleanDomain(domain: string) { + if (!domain) { + return ''; + } + return extractDomain(domain).replace(/[\W.]/g, ''); +} diff --git a/app/utils/push_proxy.ts b/app/utils/push_proxy.ts index e207c9841..7bfba76c2 100644 --- a/app/utils/push_proxy.ts +++ b/app/utils/push_proxy.ts @@ -3,16 +3,30 @@ import {Alert} from 'react-native'; +import {storePushDisabledInServerAcknowledged} from '@actions/app/global'; +import {getPushDisabledInServerAcknowledged} from '@app/queries/app/global'; import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import EphemeralStore from '@store/ephemeral_store'; +import {extractCleanDomain} from './helpers'; + import type {IntlShape} from 'react-intl'; -export function canReceiveNotifications(serverUrl: string, verification: string, intl: IntlShape) { +export async function pushDisabledInServerAck(serverUrl: string) { + const extractedDomain = extractCleanDomain(serverUrl); + const pushServerDisabledAck = await getPushDisabledInServerAcknowledged(extractedDomain); + return pushServerDisabledAck; +} + +export async function canReceiveNotifications(serverUrl: string, verification: string, intl: IntlShape) { + const a = await pushDisabledInServerAck(serverUrl); + switch (verification) { case PUSH_PROXY_RESPONSE_NOT_AVAILABLE: EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_NOT_AVAILABLE); - alertPushProxyError(intl); + if (!a) { + alertPushProxyError(intl, serverUrl); + } break; case PUSH_PROXY_RESPONSE_UNKNOWN: EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_UNKNOWN); @@ -23,7 +37,14 @@ export function canReceiveNotifications(serverUrl: string, verification: string, } } -export function alertPushProxyError(intl: IntlShape) { +const handleAlertResponse = async (buttonIndex: number, serverUrl: string) => { + if (buttonIndex === 0) { + // User clicked "Okay" acknowledging that the push notifications are disabled on that server + await storePushDisabledInServerAcknowledged(extractCleanDomain(serverUrl)); + } +}; + +export function alertPushProxyError(intl: IntlShape, serverUrl: string) { Alert.alert( intl.formatMessage({ id: 'alert.push_proxy_error.title', @@ -35,6 +56,7 @@ export function alertPushProxyError(intl: IntlShape) { }), [{ text: intl.formatMessage({id: 'alert.push_proxy.button', defaultMessage: 'Okay'}), + onPress: () => handleAlertResponse(0, serverUrl), }], ); }