Skip to content

Commit

Permalink
notifs: Warn about notifications soon to be disabled (not yet with ba…
Browse files Browse the repository at this point in the history
…nner)

Soon, we'll add a snoozable banner on the home screen. But for now:

- Warning icon / subtitle text on the "Notifications" row on the
  settings screen
- Warning row in the notification-settings screen
- Warning icon on the "Pick account" screen
  • Loading branch information
chrisbobbe authored and gnprice committed Jan 23, 2024
1 parent cd1eebd commit b30969b
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 30 deletions.
92 changes: 69 additions & 23 deletions src/account/AccountItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ import { accountSwitch } from './accountActions';
import { useNavigation } from '../react-navigation';
import {
chooseNotifProblemForShortText,
kPushNotificationsEnabledEndDoc,
notifProblemShortText,
pushNotificationsEnabledEndTimestampWarning,
} from '../settings/NotifTroubleshootingScreen';
import { getRealmName } from '../directSelectors';
import { getGlobalSettings, getRealmName } from '../directSelectors';
import { getHaveServerData } from '../haveServerDataSelectors';
import { useDateRefreshedAtInterval } from '../reactUtils';
import { openLinkWithUserPreference } from '../utils/openLink';
import * as logging from '../utils/logging';

const styles = createStyleSheet({
wrapper: {
Expand Down Expand Up @@ -69,6 +74,8 @@ export default function AccountItem(props: Props): Node {
const navigation = useNavigation();
const dispatch = useGlobalDispatch();

const globalSettings = useGlobalSelector(getGlobalSettings);

const isActiveAccount = useGlobalSelector(state => getIsActiveAccount(state, { email, realm }));

// Don't show the "remove account" button (the "trash" icon) for the
Expand All @@ -80,6 +87,8 @@ export default function AccountItem(props: Props): Node {
const backgroundItemColor = isLoggedIn ? 'hsla(177, 70%, 47%, 0.1)' : 'hsla(0,0%,50%,0.1)';
const textColor = isLoggedIn ? BRAND_COLOR : 'gray';

const dateNow = useDateRefreshedAtInterval(60_000);

const activeAccountState = useGlobalSelector(tryGetActiveAccountState);
// The fallback text '(unknown organization name)' is never expected to
// appear in the UI. As of writing, notifProblemShortText doesn't use its
Expand All @@ -88,36 +97,73 @@ export default function AccountItem(props: Props): Node {
// `realmName` will be the real realm name, not the fallback.
// TODO(#5005) look for server data even when this item's account is not
// the active one.
const realmName =
isActiveAccount && activeAccountState != null && getHaveServerData(activeAccountState)
? getRealmName(activeAccountState)
: '(unknown organization name)';
let realmName = '(unknown organization name)';
let expiryWarning = null;
if (isActiveAccount && activeAccountState != null && getHaveServerData(activeAccountState)) {
realmName = getRealmName(activeAccountState);
expiryWarning = silenceServerPushSetupWarnings
? null
: pushNotificationsEnabledEndTimestampWarning(activeAccountState, dateNow);
}
const singleNotifProblem = chooseNotifProblemForShortText({
report: notificationReport,
ignoreServerHasNotEnabled: silenceServerPushSetupWarnings,
});

const handlePressNotificationWarning = React.useCallback(() => {
if (singleNotifProblem == null) {
if (expiryWarning == null && singleNotifProblem == null) {
logging.warn('AccountItem: Notification warning pressed with nothing to show');
return;
}

if (singleNotifProblem != null) {
Alert.alert(
_('Notifications'),
_(notifProblemShortText(singleNotifProblem, realmName)),
[
{ text: _('Cancel'), style: 'cancel' },
{
text: _('Details'),
onPress: () => {
dispatch(accountSwitch({ realm, email }));
navigation.push('notifications');
},
style: 'default',
},
],
{ cancelable: true },
);
return;
}
Alert.alert(
_('Notifications'),
_(notifProblemShortText(singleNotifProblem, realmName)),
[
{ text: _('Cancel'), style: 'cancel' },
{
text: _('Details'),
onPress: () => {
dispatch(accountSwitch({ realm, email }));
navigation.push('notifications');

if (expiryWarning != null) {
Alert.alert(
_('Notifications'),
_(expiryWarning.text),
[
{ text: _('Cancel'), style: 'cancel' },
{
text: _('Details'),
onPress: () => {
openLinkWithUserPreference(kPushNotificationsEnabledEndDoc, globalSettings);
},
style: 'default',
},
style: 'default',
},
],
{ cancelable: true },
);
}, [email, singleNotifProblem, realm, realmName, navigation, dispatch, _]);
],
{ cancelable: true },
);
}
}, [
email,
singleNotifProblem,
expiryWarning,
realm,
realmName,
globalSettings,
navigation,
dispatch,
_,
]);

return (
<Touchable style={styles.wrapper} onPress={() => props.onSelect(props.account)}>
Expand All @@ -139,7 +185,7 @@ export default function AccountItem(props: Props): Node {
<ZulipTextIntl style={styles.signedOutText} text="Signed out" numberOfLines={1} />
)}
</View>
{singleNotifProblem != null && (
{(singleNotifProblem != null || expiryWarning != null) && (
<Pressable style={styles.icon} hitSlop={12} onPress={handlePressNotificationWarning}>
{({ pressed }) => (
<IconAlertTriangle
Expand Down
56 changes: 55 additions & 1 deletion src/settings/NotifTroubleshootingScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as MailComposer from 'expo-mail-composer';
import { nativeApplicationVersion } from 'expo-application';
// $FlowFixMe[untyped-import]
import uniq from 'lodash.uniq';
import subDays from 'date-fns/subDays';

import type { RouteProp } from '../react-navigation';
import type { AppNavigationProp } from '../nav/AppNavigator';
Expand Down Expand Up @@ -49,6 +50,8 @@ import { ApiError } from '../api/apiErrors';
import NavRow from '../common/NavRow';
import RowGroup from '../common/RowGroup';
import TextRow from '../common/TextRow';
import type { PerAccountState } from '../reduxTypes';
import { useDateRefreshedAtInterval } from '../reactUtils';

const {
Notifications, // android
Expand Down Expand Up @@ -272,6 +275,8 @@ export type NotificationReport = {|
+zulipVersion: ZulipVersion,
+zulipFeatureLevel: number,
+pushNotificationsEnabled: boolean,
+pushNotificationsEnabledEndTimestamp: number | null,
+endTimestampIsNear: boolean,
+offlineNotification: boolean,
+onlineNotification: boolean,
+streamNotification: boolean,
Expand All @@ -289,6 +294,48 @@ function jsonifyNotificationReport(report: NotificationReport): string {
);
}

export const kPushNotificationsEnabledEndDoc: URL = new URL(
'https://zulip.com/help/self-hosted-billing#upgrades-for-legacy-customers',
);

export const pushNotificationsEnabledEndTimestampWarning = (
state: PerAccountState,
dateNow: Date,
): {| text: LocalizableText, reactText: LocalizableReactText |} | null => {
if (!getHaveServerData(state)) {
return null;
}
const realmState = getRealm(state);
const timestamp = realmState.pushNotificationsEnabledEndTimestamp;
if (timestamp == null) {
return null;
}
const timestampMs = timestamp * 1000;
if (subDays(new Date(timestampMs), 15) > dateNow) {
return null;
}
const realmName = realmState.name;
const twentyFourHourTime = realmState.twentyFourHourTime;
const message = twentyFourHourTime
? 'On {endTimestamp, date, short} at {endTimestamp, time, ::H:mm z}, push notifications will be disabled for {realmName}.'
: 'On {endTimestamp, date, short} at {endTimestamp, time, ::h:mm z}, push notifications will be disabled for {realmName}.';
return {
text: {
text: message,
values: { endTimestamp: timestampMs, realmName },
},
reactText: {
text: message,
values: {
endTimestamp: timestampMs,
realmName: (
<ZulipText inheritColor inheritFontSize style={{ fontWeight: 'bold' }} text={realmName} />
),
},
},
};
};

/**
* Generate and return a NotificationReport for all accounts we know about.
*/
Expand All @@ -302,6 +349,8 @@ export function useNotificationReportsByIdentityKey(): Map<string, NotificationR
const accounts = useGlobalSelector(getAccounts);
const activeAccountState = useGlobalSelector(tryGetActiveAccountState);

const dateNow = useDateRefreshedAtInterval(60_000);

return React.useMemo(
() =>
new Map(
Expand All @@ -324,6 +373,11 @@ export function useNotificationReportsByIdentityKey(): Map<string, NotificationR
zulipVersion: getServerVersion(activeAccountState),
zulipFeatureLevel: getZulipFeatureLevel(activeAccountState),
pushNotificationsEnabled: getRealm(activeAccountState).pushNotificationsEnabled,
pushNotificationsEnabledEndTimestamp:
getRealm(activeAccountState).pushNotificationsEnabledEndTimestamp,
endTimestampIsNear:
pushNotificationsEnabledEndTimestampWarning(activeAccountState, dateNow)
!= null,
offlineNotification: getSettings(activeAccountState).offlineNotification,
onlineNotification: getSettings(activeAccountState).onlineNotification,
streamNotification: getSettings(activeAccountState).streamNotification,
Expand Down Expand Up @@ -375,7 +429,7 @@ export function useNotificationReportsByIdentityKey(): Map<string, NotificationR
];
}),
),
[nativeState, accounts, activeAccountState, pushToken, platform],
[nativeState, accounts, activeAccountState, pushToken, platform, dateNow],
);
}

Expand Down
22 changes: 22 additions & 0 deletions src/settings/PerAccountNotificationSettingsGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ import {
NotificationProblem,
notifProblemShortReactText,
chooseNotifProblemForShortText,
pushNotificationsEnabledEndTimestampWarning,
kPushNotificationsEnabledEndDoc,
} from './NotifTroubleshootingScreen';
import { keyOfIdentity } from '../account/accountMisc';
import { ApiError } from '../api/apiErrors';
import { showErrorAlert } from '../utils/info';
import * as logging from '../utils/logging';
import { TranslationContext } from '../boot/TranslationProvider';
import { setSilenceServerPushSetupWarnings } from '../account/accountActions';
import { useDateRefreshedAtInterval } from '../reactUtils';

type Props = $ReadOnly<{|
navigation: AppNavigationMethods,
Expand All @@ -48,6 +51,8 @@ export default function PerAccountNotificationSettingsGroup(props: Props): Node
const dispatch = useDispatch();
const _ = React.useContext(TranslationContext);

const dateNow = useDateRefreshedAtInterval(60_000);

const auth = useSelector(getAuth);
const identity = useSelector(getIdentity);
const notificationReportsByIdentityKey = useNotificationReportsByIdentityKey();
Expand All @@ -59,6 +64,8 @@ export default function PerAccountNotificationSettingsGroup(props: Props): Node
const realmName = useSelector(getRealmName);
const zulipFeatureLevel = useSelector(getZulipFeatureLevel);
const pushNotificationsEnabled = useSelector(state => getRealm(state).pushNotificationsEnabled);
const perAccountState = useSelector(state => state);
const expiryWarning = pushNotificationsEnabledEndTimestampWarning(perAccountState, dateNow);
const silenceServerPushSetupWarnings = useSelector(getSilenceServerPushSetupWarnings);
const offlineNotification = useSelector(state => getSettings(state).offlineNotification);
const onlineNotification = useSelector(state => getSettings(state).onlineNotification);
Expand All @@ -68,6 +75,10 @@ export default function PerAccountNotificationSettingsGroup(props: Props): Node

const pushToken = useGlobalSelector(state => getGlobalSession(state).pushToken);

const handleExpiryWarningPress = React.useCallback(() => {
openLinkWithUserPreference(kPushNotificationsEnabledEndDoc, globalSettings);
}, [globalSettings]);

const handleSilenceWarningsChange = React.useCallback(() => {
dispatch(setSilenceServerPushSetupWarnings(!silenceServerPushSetupWarnings));
}, [dispatch, silenceServerPushSetupWarnings]);
Expand Down Expand Up @@ -163,6 +174,17 @@ export default function PerAccountNotificationSettingsGroup(props: Props): Node
}

const children = [];
if (expiryWarning != null) {
children.push(
<NavRow
key="expiry"
type="external"
leftElement={{ type: 'icon', Component: IconAlertTriangle, color: kWarningColor }}
title={expiryWarning.reactText}
onPress={handleExpiryWarningPress}
/>,
);
}
if (pushNotificationsEnabled) {
children.push(
<SwitchRow
Expand Down
27 changes: 21 additions & 6 deletions src/settings/SettingsScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,31 @@ import {
useNotificationReportsByIdentityKey,
chooseNotifProblemForShortText,
notifProblemShortReactText,
pushNotificationsEnabledEndTimestampWarning,
} from './NotifTroubleshootingScreen';
import { noTranslation } from '../i18n/i18n';
import { keyOfIdentity } from '../account/accountMisc';
import languages from './languages';
import { getRealmName } from '../directSelectors';
import { useDateRefreshedAtInterval } from '../reactUtils';

type Props = $ReadOnly<{|
navigation: AppNavigationProp<'settings'>,
route: RouteProp<'settings', void>,
|}>;

export default function SettingsScreen(props: Props): Node {
const dateNow = useDateRefreshedAtInterval(60_000);

const theme = useGlobalSelector(state => getGlobalSettings(state).theme);
const browser = useGlobalSelector(state => getGlobalSettings(state).browser);
const globalSettings = useGlobalSelector(getGlobalSettings);
const markMessagesReadOnScroll = globalSettings.markMessagesReadOnScroll;
const language = useGlobalSelector(state => getGlobalSettings(state).language);

const perAccountState = useSelector(state => state);
const expiryWarning = pushNotificationsEnabledEndTimestampWarning(perAccountState, dateNow);

const zulipVersion = useSelector(getServerVersion);
const identity = useSelector(getIdentity);
const notificationReportsByIdentityKey = useNotificationReportsByIdentityKey();
Expand Down Expand Up @@ -101,12 +108,20 @@ export default function SettingsScreen(props: Props): Node {
title="Notifications"
{...(() => {
const problem = chooseNotifProblemForShortText({ report: notificationReport });
return (
problem != null && {
leftElement: { type: 'icon', Component: IconAlertTriangle, color: kWarningColor },
subtitle: notifProblemShortReactText(problem, realmName),
}
);
if (expiryWarning == null && problem == null) {
return;
}
let subtitle = undefined;
if (problem != null) {
subtitle = notifProblemShortReactText(problem, realmName);
} else if (expiryWarning != null) {
subtitle = expiryWarning.reactText;
}
invariant(subtitle != null, 'expected non-null `expiryWarning` or `problem`');
return {
leftElement: { type: 'icon', Component: IconAlertTriangle, color: kWarningColor },
subtitle,
};
})()}
onPress={() => {
navigation.push('notifications');
Expand Down
2 changes: 2 additions & 0 deletions static/translations/messages_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
"Terms for {realmName}": "Terms for {realmName}",
"Dismiss": "Dismiss",
"Push notifications are not enabled for {realmName}.": "Push notifications are not enabled for {realmName}.",
"On {endTimestamp, date, short} at {endTimestamp, time, ::H:mm z}, push notifications will be disabled for {realmName}.": "On {endTimestamp, date, short} at {endTimestamp, time, ::H:mm z}, push notifications will be disabled for {realmName}.",
"On {endTimestamp, date, short} at {endTimestamp, time, ::h:mm z}, push notifications will be disabled for {realmName}.": "On {endTimestamp, date, short} at {endTimestamp, time, ::h:mm z}, push notifications will be disabled for {realmName}.",
"The Zulip server at {realm} has not yet registered your device token. A request is in progress.": "The Zulip server at {realm} has not yet registered your device token. A request is in progress.",
"The Zulip server at {realm} has not yet registered your device token.": "The Zulip server at {realm} has not yet registered your device token.",
"Registration failed": "Registration failed",
Expand Down

0 comments on commit b30969b

Please sign in to comment.