Skip to content

Commit

Permalink
FEATURE: Support notifying for self-hosted instances (#208)
Browse files Browse the repository at this point in the history
This restores BackgroundFetch processing which unlocks a few nice-to-haves.

First, the app's badge count will now update in the background. The job
runs at most every 15 minutes (as per iOS restrictions).

Sites not hosted by Discourse (i.e. sites without real push notifications)
will get an extra benefit: the background job will trigger a local
notification if the site's unread count has increased since the last check.

This also includes dependency bumps, including babel/traverse.
  • Loading branch information
pmusaraj authored May 2, 2024
1 parent 1c907ec commit 95c720f
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 308 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ PLATFORMS
ruby

DEPENDENCIES
cocoapods (>= 1.13.2)
cocoapods (>= 1.14.2)
fastlane
fastlane-plugin-increment_version_code
fastlane-plugin-versioning
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 189
versionCode 194
versionName MYAPP_VERSION
}

Expand Down
15 changes: 10 additions & 5 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ fastlane_require "base64"
fastlane_require "fileutils"
fastlane_require "json"

IOS_APP_VERSION = "1.8.14"
ANDROID_APP_VERSION = "1.8.12"
IOS_APP_VERSION = "1.8.15"
ANDROID_APP_VERSION = "1.8.14" # run `fastlane bootstrap` after editing this
PROJECT_NAME = "Discourse"
IOS_TEAM_ID = "6T3LU73T8S"
KEYS_REPOSITORY = "git@github.com:discourse-org/discourse-mobile-keys.git"
Expand Down Expand Up @@ -150,7 +150,7 @@ platform :ios do
lane :certs do
system("rm -rf ~/Library/MobileDevice/Provisioning\ Profiles/*.mobile*")

register_devices(devices_file: File.expand_path("./keys/ios-beta-devices.txt"))
# register_devices(devices_file: File.expand_path("./keys/ios-beta-devices.txt"))

match(
force: true,
Expand Down Expand Up @@ -184,7 +184,11 @@ platform :ios do
workspace: "./ios/#{PROJECT_NAME}.xcworkspace"
)

pilot(skip_waiting_for_build_processing: true, api_key_path: "fastlane/keys/appstoreconnect-key.json")
pilot(
skip_waiting_for_build_processing: true,
api_key_path: "fastlane/keys/appstoreconnect-key.json"
# notify_external_testers: false
)
end

desc "Install on connected device"
Expand Down Expand Up @@ -233,8 +237,9 @@ platform :android do
project_dir: "android/"
)

supply(
upload_to_play_store(
track: "alpha",
json_key: "fastlane/google-play-api-secret.json",
skip_upload_apk: true
)
end
Expand Down
6 changes: 4 additions & 2 deletions ios/Discourse.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Discourse/Pods-Discourse-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RNBackgroundFetch/TSBackgroundFetch.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
Expand All @@ -372,6 +373,7 @@
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TSBackgroundFetch.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
Expand Down Expand Up @@ -477,7 +479,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 443;
CURRENT_PROJECT_VERSION = 455;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 6T3LU73T8S;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S;
Expand Down Expand Up @@ -522,7 +524,7 @@
CODE_SIGN_ENTITLEMENTS = Discourse/Discourse.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 443;
CURRENT_PROJECT_VERSION = 455;
DEVELOPMENT_TEAM = 6T3LU73T8S;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6T3LU73T8S;
ENABLE_BITCODE = NO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@
ReferencedContainer = "container:Discourse.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "IDEPreferLogStreaming"
value = "YES"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand Down
4 changes: 2 additions & 2 deletions ios/Discourse/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.8.14</string>
<string>1.8.15</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand All @@ -37,7 +37,7 @@
<dict/>
</array>
<key>CFBundleVersion</key>
<string>443</string>
<string>455</string>
<key>Fabric</key>
<dict>
<key>APIKey</key>
Expand Down
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ PODS:
- React-jsi (= 0.71.7)
- React-logger (= 0.71.7)
- React-perflogger (= 0.71.7)
- RNBackgroundFetch (4.2.1):
- RNBackgroundFetch (4.2.3):
- React-Core
- RNCAsyncStorage (1.17.11):
- React-Core
Expand Down Expand Up @@ -628,7 +628,7 @@ SPEC CHECKSUMS:
React-RCTVibration: 08f132cad9896458776f37c112e71d60aef1c6ae
React-runtimeexecutor: c5c89f8f543842dd864b63ded1b0bbb9c9445328
ReactCommon: dbfbe2f7f3c5ce4ce44f43f2fd0d5950d1eb67c5
RNBackgroundFetch: 501c34ad6e880818ba17e7840b23f20a2d112923
RNBackgroundFetch: b6cb89fa8ff0e1d8a245b19caca2944304b95fbf
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDeviceInfo: bf8a32acbcb875f568217285d1793b0e8588c974
Expand Down
4 changes: 2 additions & 2 deletions ios/ShareExtension/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.8.14</string>
<string>1.8.15</string>
<key>CFBundleVersion</key>
<string>443</string>
<string>455</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
Expand Down
127 changes: 58 additions & 69 deletions js/Discourse.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import * as RNLocalize from 'react-native-localize';
import {addShortcutListener} from 'react-native-siri-shortcut';
import {enableScreens} from 'react-native-screens';

// import BackgroundFetch from './platforms/background-fetch';
import BackgroundFetch from './platforms/background-fetch';

const {DiscourseKeyboardShortcuts} = NativeModules;

Expand Down Expand Up @@ -79,6 +79,7 @@ class Discourse extends React.Component {
super(props);
this._siteManager = new SiteManager();
this._refresh = this._refresh.bind(this);
this._initBackgroundFetch = this._initBackgroundFetch.bind(this);

this._handleAppStateChange = nextAppState => {
console.log('Detected appState change: ' + nextAppState);
Expand All @@ -92,36 +93,29 @@ class Discourse extends React.Component {

clearTimeout(this.refreshTimerId);
this.refreshTimerId = setTimeout(this._refresh, 30000);

if (Platform.OS === 'android') {
AsyncStorage.getItem('@AndroidMessageUrl').then(url => {
if (url) {
this.openUrl(url);
AsyncStorage.removeItem('@AndroidMessageUrl');
}
});
}
}
};

this._handleOpenUrl = this._handleOpenUrl.bind(this);

if (Platform.OS === 'ios') {
PushNotificationIOS.addEventListener('notification', e =>
this._handleRemoteNotification(e),
this._handleNotification(e),
);

// PushNotificationIOS.addEventListener('localNotification', e =>
// this._handleRemoteNotification(e),
// );
// local notifications, triggered via background fetch
// for non-hosted sites only (sites where hasPush = false)
PushNotificationIOS.addEventListener('localNotification', e =>
this._handleNotification(e),
);

PushNotificationIOS.addEventListener('register', s => {
this._siteManager.registerClientId(s);
});

PushNotificationIOS.getInitialNotification().then(e => {
if (e) {
this._handleRemoteNotification(e);
this._handleNotification(e);
}
});
}
Expand Down Expand Up @@ -157,8 +151,9 @@ class Discourse extends React.Component {
ToastAndroid.show(message, ToastAndroid.LONG);
});

// notification received while app is in background/closed
firebaseMessaging.setBackgroundMessageHandler(async remoteMessage => {
// notification clicked while app is in background/closed
firebaseMessaging.onNotificationOpenedApp(async remoteMessage => {
console.log('onNotificationOpenedApp');
let url = null;

if (remoteMessage.data.payload) {
Expand All @@ -171,7 +166,7 @@ class Discourse extends React.Component {
}

if (url) {
AsyncStorage.setItem('@AndroidMessageUrl', url);
this.openUrl(url);
}
});
}
Expand Down Expand Up @@ -221,29 +216,18 @@ class Discourse extends React.Component {
}
}

// _handleLocalNotification(e) {
// console.log("got local notification", e);
// if (
// AppState.currentState !== "active" &&
// e._data &&
// e._data.discourse_url
// ) {
// this.openUrl(e._data.discourse_url);
// }
// }

_handleRemoteNotification(e) {
console.log('got remote notification', e);
if (e._data && e._data.discourse_url) {
this._siteManager
.setActiveSite(e._data.discourse_url)
.then(activeSite => {
let supportsDelegatedAuth = false;
if (this._siteManager.supportsDelegatedAuth(activeSite)) {
supportsDelegatedAuth = true;
}
this.openUrl(e._data.discourse_url, supportsDelegatedAuth);
});
_handleNotification(e) {
console.log('got notification', e);
const url = e._data && e._data.discourse_url;

if (url) {
this._siteManager.setActiveSite(url).then(activeSite => {
let supportsDelegatedAuth = false;
if (this._siteManager.supportsDelegatedAuth(activeSite)) {
supportsDelegatedAuth = true;
}
this.openUrl(url, supportsDelegatedAuth);
});
}
}

Expand Down Expand Up @@ -355,40 +339,45 @@ class Discourse extends React.Component {
}
}
});
}

// BackgroundFetch register (15-minute minimum interval allowed)
// this._initBackgroundFetch();
// delay here may be redundant, but it ensures site data is loaded
setTimeout(this._initBackgroundFetch, 2000);
}

clearTimeout(this.refreshTimerId);
this.refreshTimerId = setTimeout(this._refresh, 30000);
}

// TODO: Restore background fetch
// async _initBackgroundFetch() {
// BackgroundFetch event handler.
// const onEvent = async taskId => {
// console.log('[BackgroundFetch] task: ', taskId);
// // Do your background work...
// this._siteManager = new SiteManager();
// await this._siteManager.backgroundRefresh();
// // IMPORTANT: You must signal to the OS that your task is complete.
// BackgroundFetch.finish(taskId);
// };
// // Timeout callback is executed when your Task has exceeded its allowed running-time.
// // You must stop what you're doing immediately BackgroundFetch.finish(taskId)
// const onTimeout = async taskId => {
// console.warn('[BackgroundFetch] TIMEOUT task: ', taskId);
// BackgroundFetch.finish(taskId);
// };
// // Initialize BackgroundFetch only once when component mounts.
// let status = await BackgroundFetch.configure(
// {minimumFetchInterval: 15},
// onEvent,
// onTimeout,
// );
// console.log('[BackgroundFetch] configure status: ', status);
// }
// runs on background, ever 15 mins max
// updates site unread counts, app badge
// and for non-hosted sites, triggers a local notification if new count > old count
async _initBackgroundFetch() {
// uncomment to test iOS background
// this will run on app live reload
// await this._siteManager.iOSbackgroundRefresh();

const onEvent = async taskId => {
console.log('[BackgroundFetch] task: ', taskId);
await this._siteManager.iOSbackgroundRefresh();

// You must signal to the OS that your task is complete.
BackgroundFetch.finish(taskId);
};

// Timeout callback is executed when your Task has exceeded its allowed running-time.
// You must stop what you're doing immediately BackgroundFetch.finish(taskId)
const onTimeout = async taskId => {
console.warn('[BackgroundFetch] TIMEOUT task: ', taskId);
BackgroundFetch.finish(taskId);
};
// Initialize BackgroundFetch only once when component mounts.
let status = await BackgroundFetch.configure(
{minimumFetchInterval: 15},
onEvent,
onTimeout,
);
console.log('[BackgroundFetch] configure status: ', status);
}

async _refresh() {
clearTimeout(this.refreshTimerId);
Expand Down
5 changes: 5 additions & 0 deletions js/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
"one": "%{count} message in your %{group_name} inbox",
"other": "%{count} messages in your %{group_name} inbox"
},
"generic_notification_title": {
"one": "%{count} new notification",
"other": "%{count} new notifications"
},
"generic_notification_body": "See what's new on %{url}",
"incorrect_url": "Sorry, %{term} is not a correct URL to a Discourse forum or does not support mobile APIs, have owner upgrade Discourse to latest!",
"cannot_load_url": "That URL could not be added to DiscourseHub.",
"last_updated": "Last Updated:",
Expand Down
Loading

0 comments on commit 95c720f

Please # to comment.