Skip to content

Commit 7df638e

Browse files
committed
NotificationsScreen: Add warning to highlight permissions not granted
1 parent 9680d46 commit 7df638e

File tree

7 files changed

+180
-5
lines changed

7 files changed

+180
-5
lines changed

android/app/src/main/java/com/zulipmobile/notifications/NotificationsModule.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.os.Bundle;
44
import android.util.Log;
55
import androidx.annotation.Nullable;
6+
import androidx.core.app.NotificationManagerCompat;
67
import com.facebook.react.bridge.Arguments;
78
import com.facebook.react.bridge.Promise;
89
import com.facebook.react.bridge.ReactApplicationContext;
@@ -54,4 +55,20 @@ public void readInitialNotification(Promise promise) {
5455
initialNotification = null;
5556
}
5657
}
58+
59+
/**
60+
* Tell the JavaScript caller whether notifications are not blocked.
61+
*/
62+
// Ideally we could subscribe to changes in this value, but there
63+
// doesn't seem to be an API for that. The caller can poll, e.g., by
64+
// re-checking when the user has returned to the app, which they might
65+
// do after changing the notification settings.
66+
@ReactMethod
67+
public void areNotificationsEnabled(Promise promise) {
68+
final NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat
69+
.from(getReactApplicationContext());
70+
71+
// https://developer.android.com/reference/androidx/core/app/NotificationManagerCompat#areNotificationsEnabled()
72+
promise.resolve(notificationManagerCompat.areNotificationsEnabled());
73+
}
5774
}

ios/ZulipMobile.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
8B40B9F327F7AEEC00D33897 /* zulip-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3787526927F5503200B81DB8 /* zulip-icons.ttf */; };
1515
8B44AB3E295B7601003D41A8 /* ZLPConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B44AB3A295B7263003D41A8 /* ZLPConstants.swift */; };
1616
8B44AB40295B7D4D003D41A8 /* ZLPConstantsBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B44AB3F295B7D45003D41A8 /* ZLPConstantsBridge.m */; };
17+
8BD55E9B295CB7A10091C181 /* ZLPNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD55E9A295CB61E0091C181 /* ZLPNotifications.swift */; };
18+
8BD55E9C295CB7A10091C181 /* ZLPNotificationsBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD55E99295CB61E0091C181 /* ZLPNotificationsBridge.m */; };
1719
8BE55043253A2B6600B0BC8A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8BE55041253A2B6600B0BC8A /* LaunchScreen.storyboard */; };
1820
A148FEFC1E9D8CB900479280 /* zulip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A148FEFB1E9D8CB900479280 /* zulip.mp3 */; };
1921
A65C1ECEE03CA6692FC807AA /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C68E38F2AA306A2C56A4B3 /* ExpoModulesProvider.swift */; };
@@ -37,6 +39,8 @@
3739
74F3CD22CB932FA7EEE0BB66 /* libPods-ZulipMobile.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ZulipMobile.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3840
8B44AB3A295B7263003D41A8 /* ZLPConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZLPConstants.swift; path = ZulipMobile/ZLPConstants.swift; sourceTree = "<group>"; };
3941
8B44AB3F295B7D45003D41A8 /* ZLPConstantsBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ZLPConstantsBridge.m; path = ZulipMobile/ZLPConstantsBridge.m; sourceTree = "<group>"; };
42+
8BD55E99295CB61E0091C181 /* ZLPNotificationsBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ZLPNotificationsBridge.m; path = ZulipMobile/ZLPNotificationsBridge.m; sourceTree = "<group>"; };
43+
8BD55E9A295CB61E0091C181 /* ZLPNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZLPNotifications.swift; path = ZulipMobile/ZLPNotifications.swift; sourceTree = "<group>"; };
4044
8BE55042253A2B6600B0BC8A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = ZulipMobile/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
4145
A148FEFB1E9D8CB900479280 /* zulip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = zulip.mp3; sourceTree = "<group>"; };
4246
CFA67D1F1EC23BCB0070048E /* UtilManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UtilManager.m; path = ZulipMobile/UtilManager.m; sourceTree = "<group>"; };
@@ -62,6 +66,8 @@
6266
13B07FAE1A68108700A75B9A /* ZulipMobile */ = {
6367
isa = PBXGroup;
6468
children = (
69+
8BD55E9A295CB61E0091C181 /* ZLPNotifications.swift */,
70+
8BD55E99295CB61E0091C181 /* ZLPNotificationsBridge.m */,
6571
8B44AB3F295B7D45003D41A8 /* ZLPConstantsBridge.m */,
6672
8B44AB3A295B7263003D41A8 /* ZLPConstants.swift */,
6773
3C4249EC1EF6E16500D245F1 /* ZulipMobile.entitlements */,
@@ -370,6 +376,8 @@
370376
CFA67D201EC23BCB0070048E /* UtilManager.m in Sources */,
371377
8B44AB3E295B7601003D41A8 /* ZLPConstants.swift in Sources */,
372378
8B44AB40295B7D4D003D41A8 /* ZLPConstantsBridge.m in Sources */,
379+
8BD55E9B295CB7A10091C181 /* ZLPNotifications.swift in Sources */,
380+
8BD55E9C295CB7A10091C181 /* ZLPNotificationsBridge.m in Sources */,
373381
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
374382
13B07FC11A68108700A75B9A /* main.m in Sources */,
375383
A65C1ECEE03CA6692FC807AA /* ExpoModulesProvider.swift in Sources */,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@objc(ZLPNotifications)
2+
class ZLPNotifications: NSObject {
3+
// For why we include this, see
4+
// https://reactnative.dev/docs/0.68/native-modules-ios#exporting-constants
5+
@objc
6+
static func requiresMainQueueSetup() -> Bool {
7+
// Initialization may be done on any thread; we don't need access to
8+
// UIKit.
9+
return false
10+
}
11+
12+
/// Whether the app can receive remote notifications.
13+
// Ideally we could subscribe to changes in this value, but there
14+
// doesn't seem to be an API for that. The caller can poll, e.g., by
15+
// re-checking when the user has returned to the app, which they might
16+
// do after changing the notification settings.
17+
@objc
18+
func areNotificationsAuthorized(
19+
_ resolve: @escaping RCTPromiseResolveBlock,
20+
rejecter reject: RCTPromiseRejectBlock
21+
) -> Void {
22+
UNUserNotificationCenter.current()
23+
.getNotificationSettings(completionHandler: { (settings) -> Void in
24+
resolve(settings.authorizationStatus == UNAuthorizationStatus.authorized)
25+
})
26+
}
27+
28+
@objc
29+
func constantsToExport() -> [String: Any]! {
30+
return [:]
31+
}
32+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#import "React/RCTBridgeModule.h"
2+
3+
// Register the ZLPNotifications implementation with React Native, needed
4+
// because ZLPNotifications is in Swift:
5+
// https://reactnative.dev/docs/0.68/native-modules-ios#exporting-swift
6+
@interface RCT_EXTERN_MODULE(ZLPNotifications, NSObject)
7+
8+
RCT_EXTERN_METHOD(areNotificationsAuthorized:
9+
(RCTPromiseResolveBlock) resolve
10+
rejecter: (RCTPromiseRejectBlock) reject
11+
)
12+
13+
@end

src/common/Icons.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const IconAttach: SpecificIconType = makeIcon(Feather, 'paperclip');
111111
export const IconAttachment: SpecificIconType = makeIcon(IoniconsIcon, 'document-attach-outline');
112112
export const IconGroup: SpecificIconType = makeIcon(FontAwesome, 'group');
113113
export const IconPlus: SpecificIconType = makeIcon(Feather, 'plus');
114+
export const IconAlertTriangle: SpecificIconType = makeIcon(Feather, 'alert-triangle');
114115

115116
// WildcardMentionItem depends on this being square.
116117
export const IconWildcardMention: SpecificIconType = makeIcon(FontAwesome, 'bullhorn');

src/settings/NotificationsScreen.js

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* @flow strict-local */
22

3-
import React, { useCallback } from 'react';
3+
import React, { useCallback, useContext } from 'react';
44
import type { Node } from 'react';
5-
import { Platform, Linking, NativeModules } from 'react-native';
5+
import { Alert, Platform, Linking, NativeModules } from 'react-native';
66
import OpenNotification from 'react-native-open-notification';
77

88
import type { RouteProp } from '../react-navigation';
@@ -14,14 +14,71 @@ import Screen from '../common/Screen';
1414
import * as api from '../api';
1515
import ServerPushSetupBanner from '../common/ServerPushSetupBanner';
1616
import NestedNavRow from '../common/NestedNavRow';
17+
import { useAppState } from '../reactNativeUtils';
18+
import { IconAlertTriangle } from '../common/Icons';
19+
import type { LocalizableText } from '../types';
20+
import { TranslationContext } from '../boot/TranslationProvider';
1721

18-
const { ZLPConstants } = NativeModules;
22+
const {
23+
ZLPConstants,
24+
Notifications, // android
25+
ZLPNotifications, // ios
26+
} = NativeModules;
1927

2028
type Props = $ReadOnly<{|
2129
navigation: AppNavigationProp<'notifications'>,
2230
route: RouteProp<'notifications', void>,
2331
|}>;
2432

33+
/**
34+
* A problem in the system notification settings that we should warn about.
35+
*/
36+
enum SystemSettingsWarning {
37+
Disabled = 0,
38+
// TODO: …more, e.g.:
39+
// TODO(#5484): Android notification sound file missing
40+
// TODO(#438): Badge count disabled (once iOS supports it)
41+
}
42+
43+
function systemSettingsWarningMsg(warning: SystemSettingsWarning): LocalizableText {
44+
switch (warning) {
45+
case SystemSettingsWarning.Disabled:
46+
return 'Notifications are disabled.';
47+
}
48+
}
49+
50+
/**
51+
* An array of the `SystemSettingsWarning`s that currently apply.
52+
*/
53+
const useSystemSettingsWarnings = (): $ReadOnlyArray<SystemSettingsWarning> => {
54+
const [disabled, setDisabled] = React.useState(false);
55+
56+
// Subject to races if the native-method calls can resolve out of order
57+
// (unknown).
58+
const getAndSetDisabled = React.useCallback(async () => {
59+
setDisabled(
60+
Platform.OS === 'android'
61+
? !(await Notifications.areNotificationsEnabled())
62+
: !(await ZLPNotifications.areNotificationsAuthorized()),
63+
);
64+
}, []);
65+
66+
// Greg points out that neither iOS or Android seems to have an API for
67+
// subscribing to changes, so one has to poll, and this seems like a fine
68+
// way to do so:
69+
// https://github.com/zulip/zulip-mobile/pull/5627#discussion_r1058055540
70+
const appState = useAppState();
71+
React.useEffect(() => {
72+
getAndSetDisabled();
73+
}, [getAndSetDisabled, appState]);
74+
75+
const result = [];
76+
if (disabled) {
77+
result.push(SystemSettingsWarning.Disabled);
78+
}
79+
return result;
80+
};
81+
2582
function openSystemNotificationSettings() {
2683
if (Platform.OS === 'ios') {
2784
Linking.openURL(
@@ -48,14 +105,38 @@ function openSystemNotificationSettings() {
48105

49106
/** (NB this is a per-account screen -- these are per-account settings.) */
50107
export default function NotificationsScreen(props: Props): Node {
108+
const _ = useContext(TranslationContext);
109+
51110
const auth = useSelector(getAuth);
52111
const offlineNotification = useSelector(state => getSettings(state).offlineNotification);
53112
const onlineNotification = useSelector(state => getSettings(state).onlineNotification);
54113
const streamNotification = useSelector(state => getSettings(state).streamNotification);
55114

115+
const systemSettingsWarnings = useSystemSettingsWarnings();
116+
56117
const handleSystemSettingsPress = useCallback(() => {
118+
if (systemSettingsWarnings.length > 1) {
119+
Alert.alert(
120+
_('System settings for Zulip'),
121+
// List all warnings that apply.
122+
systemSettingsWarnings.map(w => _(systemSettingsWarningMsg(w))).join('\n\n'),
123+
[
124+
{ text: _('Cancel'), style: 'cancel' },
125+
{
126+
text: _('Open settings'),
127+
onPress: () => {
128+
openSystemNotificationSettings();
129+
},
130+
style: 'default',
131+
},
132+
],
133+
{ cancelable: true },
134+
);
135+
return;
136+
}
137+
57138
openSystemNotificationSettings();
58-
}, []);
139+
}, [systemSettingsWarnings, _]);
59140

60141
// TODO(#3999): It'd be good to show "working on it" UI feedback while a
61142
// request is pending, after the user touches a switch.
@@ -87,7 +168,28 @@ export default function NotificationsScreen(props: Props): Node {
87168
return (
88169
<Screen title="Notifications">
89170
<ServerPushSetupBanner isDismissable={false} />
90-
<NestedNavRow title="System settings for Zulip" onPress={handleSystemSettingsPress} />
171+
<NestedNavRow
172+
icon={
173+
systemSettingsWarnings.length > 0
174+
? {
175+
Component: IconAlertTriangle,
176+
color: 'hsl(40, 100%, 60%)', // Material warning-color
177+
}
178+
: undefined
179+
}
180+
title="System settings for Zulip"
181+
subtitle={(() => {
182+
switch (systemSettingsWarnings.length) {
183+
case 0:
184+
return undefined;
185+
case 1:
186+
return systemSettingsWarningMsg(systemSettingsWarnings[0]);
187+
default:
188+
return 'Multiple issues. Tap to learn more.';
189+
}
190+
})()}
191+
onPress={handleSystemSettingsPress}
192+
/>
91193
<SwitchRow
92194
label="Notifications when offline"
93195
value={offlineNotification}

static/translations/messages_en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@
224224
"Recipients": "Recipients",
225225
"Delete message": "Delete message",
226226
"System settings for Zulip": "System settings for Zulip",
227+
"Notifications are disabled.": "Notifications are disabled.",
228+
"Multiple issues. Tap to learn more.": "Multiple issues. Tap to learn more.",
227229
"Notifications when online": "Notifications when online",
228230
"Notifications when offline": "Notifications when offline",
229231
"Stream notifications": "Stream notifications",

0 commit comments

Comments
 (0)