From ea018a71fac0bce64ec387614b99753c145fb2f9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Tue, 2 Mar 2021 01:17:06 +0100 Subject: [PATCH] Improve push experience --- android/app/src/main/AndroidManifest.xml | 30 +---- .../app/tooot/generated/BasePackageList.java | 1 + .../drawable-hdpi/ic_stat_notifications.png | Bin 0 -> 577 bytes .../drawable-mdpi/ic_stat_notifications.png | Bin 0 -> 379 bytes .../drawable-xhdpi/ic_stat_notifications.png | Bin 0 -> 892 bytes .../drawable-xxhdpi/ic_stat_notifications.png | Bin 0 -> 1233 bytes .../ic_stat_notifications.png | Bin 0 -> 1894 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2038 -> 2017 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1383 -> 1175 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2715 -> 2518 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 4202 -> 4253 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 5166 -> 5585 bytes src/App.tsx | 14 +- src/components/GracefullyImage.tsx | 13 +- src/components/Menu/Row.tsx | 4 +- src/i18n/zh-Hans/screens/meSettingsPush.ts | 7 +- src/screens/Compose.tsx | 2 +- .../Compose/Root/Footer/Attachments.tsx | 2 +- src/screens/Compose/Root/Header.tsx | 12 +- src/screens/Tabs.tsx | 123 +++--------------- src/screens/Tabs/Me/Push.tsx | 36 +++-- src/screens/Tabs/Me/Root/Collections.tsx | 4 + src/screens/Tabs/Me/Settings/App.tsx | 66 +++++++++- src/screens/Tabs/Me/Settings/Dev.tsx | 2 +- src/screens/Tabs/Me/Switch.tsx | 4 +- .../Tabs/Shared/Account/Information.tsx | 8 +- .../Shared/Account/Information/Created.tsx | 2 +- .../Tabs/Shared/Account/Information/Stats.tsx | 2 +- src/screens/Tabs/utils/pushNavigate.ts | 31 +++++ src/screens/Tabs/utils/pushReceive.ts | 58 +++++++++ src/screens/Tabs/utils/pushRespond.ts | 54 ++++++++ src/startup/push.ts | 2 - .../slices/instances/push/androidDefaults.ts | 11 ++ src/utils/slices/instances/push/register.ts | 71 +++++----- src/utils/slices/instances/push/unregister.ts | 9 ++ .../slices/instances/updatePushDecode.ts | 60 ++++++++- src/utils/slices/instancesSlice.ts | 1 - src/utils/slices/settingsSlice.ts | 6 +- 38 files changed, 417 insertions(+), 218 deletions(-) create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stat_notifications.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stat_notifications.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stat_notifications.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stat_notifications.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stat_notifications.png create mode 100644 src/screens/Tabs/utils/pushNavigate.ts create mode 100644 src/screens/Tabs/utils/pushReceive.ts create mode 100644 src/screens/Tabs/utils/pushRespond.ts create mode 100644 src/utils/slices/instances/push/androidDefaults.ts diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e0a79e08..3e83a287 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,5 @@ - + @@ -12,30 +9,17 @@ - + + + + - + diff --git a/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java b/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java index dfd10e2d..16cc0d7a 100644 --- a/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java +++ b/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java @@ -24,6 +24,7 @@ public class BasePackageList { new expo.modules.lineargradient.LinearGradientPackage(), new expo.modules.localization.LocalizationPackage(), new expo.modules.location.LocationPackage(), + new expo.modules.notifications.NotificationsPackage(), new expo.modules.permissions.PermissionsPackage(), new expo.modules.screencapture.ScreenCapturePackage(), new expo.modules.securestore.SecureStorePackage(), diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_notifications.png b/android/app/src/main/res/drawable-hdpi/ic_stat_notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..713b3577e94e7a65afff03edcc676022c648bf9c GIT binary patch literal 577 zcmV-H0>1r;P){)RvlN76qa2s%hdPJBd) zU`~9}Bow8TP&1!-Te4(d(0i|J5rsQA2hQNEwfA1X{awGkPncv+Ofo$NLA{O)ga$%A z0=@j;p@C43KrcVJJ(QW#Q12^GMEu*^SudZNA7W~6?N{~`i8Hu?f2v(r9TBH8a|D-1?J(}tjSXaG?fPar{$=he`fVe$z4>NuM4WG8tBv0@ zQ~wkZzqWY!?>m&KD}JiQi&lAZ59VN3MEo_XW5SCuGwU?%$0u!v8zbUe?`XeCJ~NA8 zT`hBQ7h6Wf*8ItRQ@_a$-}jh-P>=9HXdu)h(8~`V8VL0W^zws;dW4<>=mo1K7~|_o P00000NkvXXu0mjfoihV7 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_notifications.png b/android/app/src/main/res/drawable-mdpi/ic_stat_notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..0241385fed11321acbf5d6597f93be3500eca637 GIT binary patch literal 379 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 Hu0mjfsXEz6 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 814816bd0e7b09e594a08559aec60f71309fe430..1c90ff7cfccb0e1b1b073f19e55ceac81f3b3c2d 100644 GIT binary patch delta 1155 zcmV-}1bqAF3YQ6xBYy;5NklDOi zfa1#J4p;^E-|{{H^R z&CP|4jSa6)OiZAusR_%=%P=!DLqtRbGBY!wrKN?&#zs_CRY6~0pVwbAr75HgKv`KC zD9!Zr^k8dii?y{iW~#Zl89qKf2oDd({{BAKicBWs-{$7#7#JA9`uaLjQc@V0i;D}d zU0q!vHh(sj)3%|Z0oB#j9|IsMDTx&b2?=>pKoA7@`ugJT?hYLt9UL&R-P+oUfPetR z#KbV0mX?+n92|tYx;nzb!mzu$i;9X0OifLpzrP=(Zb?ZAR#sLJ85zmfEiNviy}kY8 z0Zb#0u8R5Dhu+$i`v$HWiK8~TGAvilbbBd8KQj}_d z)PGT_2C}lUFgiL4YinzkT}MZU^OZVNMn(pA426Y;hH{F?<#N8ZxVRWMH#fXY#aUET zg!uS)20}`ZI*L|Fbwow&@Y})`r{LTka^1j*jT*>0wsIGAe#j;pF6mk1#PY zVPy#DyA)6aNFC?+_?Q(iHa2E?#Wqry&Lk44UZ|GdokC&<`K$ilTm!#)fY|#a0DNLI ztjxcpfHH=}UY8UQn_*@CB?Xi*B=)+bfY=Nx^Dil&j3Ket|4o5!XJ=<;Vjuny{*CFL z9=Le4g8K7A)9V$=c9a~+MJ*?s`{4uHl_%k-NG{2R25 Vv`_P!JQe@|002ovPDHLkV1k-&CL90& delta 1365 zcmV-b1*-a&3FiusBYyx1a7bBm000XT000XT0n*)m`~Uz28A(JzRA}DqS6e8iT@-dc z4|0~nppoO;AcmZx#AGC9MpDSJ7>A@N;=udf4^VQYWlSAltS6`h;ERh3p5?TP0Ui8~p^19T!iE@->gsBG{`@(g z3m-82{QSt@-=Dm^yy)AvZ>)#CDgf5j*2vDzj!aEW$=24Ehn1BTnVXxFj*bq6g@v&m z4pOICI4pNeUF0A{5ae=qcgJ&HULI*_X-PiV*x0ZS&wrjhBO@at8XX-~1puSStCN!x zT3lSDzP>(=I5uQtWboR=#01ZxPWJcr>Fd|8G%zqglarIu;le0nb#;|KfBsBELqoK* zv?MtR9T9-Jxj7mf9Hfzv5zYX}%E}7O&d$=ickkE-Pft&_F+V@gXB{3MvYiJ-j3Jot z@bF-?Wq)O54$n^y0+}b`&5Wot>RzV1Ho1z`!6<0(+2#4Gj&f1H0Jc>gq}* zB_))coGhiu@bEC}WMyTs56JuFIyp+ddV`Cg`G=S;pX?pYK4W~s_Rh2}0 zOG^vy3l9&csi`UMZU{tgZ?B}2o}SM8z#1ecCx=0*1NbciC{|GerKP33c6@xyb5~av z=e~=J3-$E$@V$|lnaN-gRZt9&NEfg)K7T&WDH#*>>{PgGl5%eN%-+S=MEB_)MZ3Ec!~BmhFtk!oscC^0dSUcY|L#s2;K zcj+F1Al*+kXgwK77D_JY9l=gSl4V3ugEb85t?H(gzI4?*m*? zh;nFXC>I}c9Ty2io{Kn8JSXHgt!Q}|;yEENKKVYOuMTiyV}ol7H53vOLJ0{8)ZX5% zVoL>p{5dP+=;%l)VtEwhfYtYF3W_EjO~1>7AOu|ym!Du$G zOvR%SX$Btza~g~Iv5$+VqV%`F4K(@i=slo(6Dt4|015yFfC4}Ppa4(+C;$`y|8)EU X25ubftqwK*00000NkvXXu0mjfIPZv* diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 04539af1e160854814142b69c0f8323c4235e709..bcda9e2b7dfcda958572709b84867c9ad8593495 100644 GIT binary patch literal 2518 zcmchZ`#;nD8^=GJkzpPim6bzt(6e$)t-n;*Z@ArrI^?p3wkL$V~uj~DIUf1JdI67FtBo0ad006VG z#ybmd(tk%>OlYTbZW#jr^pOqT{9J_BqGw=`y*)D4BP<~1&FDX+SJE(bEdj+mByNt^WVpt=`oQ8`eJU&IO!qT##QY%>%$N6}?#c5;_5GV@qx!W! zO}_V;4z%?4E%w1w_E(B01Xe!MP($NZP$aNL4uH1OGBKOtHPTYa5s8=CIrLT)Fz2!# zd$~TIjtf4#jCdLtyW1S~JMbhp$$Kxl$Uq)39gYGa1ql4!z+8X5Q)SYi4jmu*TwWUF z0$2i`X^ywFA10i;fFbKiJuyI(z%y;|^D>z@NdTmi9WZP_)C*dG>qvbf{;^?=4_-8p zf&1ppN`{TppgVMWWD%1a*AZ}Xu0Ocr>3@l*=|>n)$K>*3_5|v&2A-ThnCH7-ry|Rj z)D|Q@1Oz!7Z@7^4IJ+2cG!FBWZJ-BY^PPz~P{+IF$I|{kA-3YKHU6-eEjs1;)?qVB z(~?QBK?R;6V-U3(KujC)T%T+Vy}3H)G!PplXudp<&nt6M8JpEmF7)kAU;X*{`}ROp zkl*K??D@gRr?uE6s&Ip}%x~irnu}OxF81dPbgGr|;Y!!~k5B4`h9e6a(-T6O@wsmG*F29Bm39-AG;MvTo{K%% z8iBn`t@K=%3Rm+-C&GG$EIrxL4;}JNZG@%l(}t;zR1*s(YRPI|LqkLPCuC$~@Kn>- zEn`f?GF4Acugo_4D6j6Shehf$_u6M)YKZLU$Fgb;gMayU&k1wi&&skWvCS@WsdPt7 zYP^@+%Td}_%6oRc_h3YWtel+M#FEv0S%6i4ZE7KfT3oD}YZCgDrErXqmR549m+2%_ zrf3PQ{#twot!i7608fGSs&sw39NXeV6$d`FMBeL4lb5@XA1}E6rWpE|DE#D5Q;!;d z`;8q8dB7)M6;MQCqm4bA!~12Up-JAz1VVA(@Kcw!{dq=`KHsK4-%FM{U@=^Jrj*4Z zo@DVIG9GcjAyI7i+AA-}W7_Sw44I!0YZRZ^auE)6&i!roGD?u}_4o3}_+SmQ*e%wz z3uTTp=|flo>eG);-MkVzxo&Rqp=%%5n8f5{$&{?O4}*G_rkcyWJCg3_=WBD4MXech zI(&es<-rts*+00y-vu~m1eK!QjakjUK~j9_Uh&#Q$yF*;gJG|M%*u7rtR&u9u?To`O7 zwSgDj)mMvd@XF5yet-Xxxg!KgX8PP?i^NXDX3@f9Wk#w#R1f%)5RHr!8v>ztw9?~b z)hD;x;Sj!VkC4hMApI-6Be^=>U>H(|6XFQ~e9*;alxV`cEU;PZY~KNd(QMyCUr(z+ zOw7g$mZmx@P6-obysBs#k+Hxlzh@>Mu{wtc`#Rb`)V963b~f#xp~4vOElu92f*daf zlQEa?#3kQEs92>J+uTE&c}> z^*hx3=tbzo19WC%^BA|&LcRjJ*zOLWM1ha{PaU>n-i!%)opty{>~<8{@wG*OzV)8{ ztg1WvG$by38IAo6DMZj{!_SAg;TeLN^B||Wc#KPfK-496hMQTqdhk+cB9MBzq7gOK zyObiKnW|jbp?aiRqT;l_`&$7>KiHK}I3ocngP0>ez({{2l!(o&IK#Pc>!IL^Zb+5m ztf3zNC)`RW2ke`(_1<^lMW9dQ7DWZ;0^w(>Qy~V@xEkPk}tRsC03Vdb&35clw?xd-`6@3T%lSg z^^v6(OoBgh1JF7*;aTX#Rj0l*&{s_KzzFQ%LRRijDt#RrO_nT5opPhSln^ tRwMt`bYWL1y1Z>A!9&bVkxMC_H$w;Eo69eUbO`q~z{b)6Uu8jx`xmO!pkDw0 literal 2715 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 rF8K`GmvX4ghPEy~R5-T>O6lOepXw literal 4202 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 T4t9k6ctD}8X#lCwcpUjJJ6agI diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4ee74a902754f852c2d01c59f061a34d2172e49e..0b9bb48110ea3a244ec51b5869c4db6a40009734 100644 GIT binary patch literal 5585 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 f>t*px> { log('log', 'App', 'rendering App') const [localCorrupt, setLocalCorrupt] = useState() + useEffect(() => { + AppState.addEventListener('change', () => { + Notifications.setBadgeCountAsync(0) + Notifications.dismissAllNotificationsAsync() + }) + + return () => { + AppState.removeEventListener('change', () => {}) + } + }, []) + useEffect(() => { const delaySplash = async () => { log('log', 'App', 'delay splash') diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx index dad2d500..03462ac2 100644 --- a/src/components/GracefullyImage.tsx +++ b/src/components/GracefullyImage.tsx @@ -109,14 +109,11 @@ const GracefullyImage = React.memo( ) }, - (prev, next) => { - let skipUpdate = true - skipUpdate = prev.hidden === next.hidden - skipUpdate = prev.uri.preview === next.uri.preview - skipUpdate = prev.uri.original === next.uri.original - skipUpdate = prev.uri.remote === next.uri.remote - return false - } + (prev, next) => + prev.hidden === next.hidden && + prev.uri.preview === next.uri.preview && + prev.uri.original === next.uri.original && + prev.uri.remote === next.uri.remote ) const styles = StyleSheet.create({ diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index d3b3a45a..f8664cae 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -3,9 +3,9 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { ColorDefinitions } from '@utils/styles/themes' import React, { useMemo } from 'react' -import { StyleSheet, Switch, Text, View } from 'react-native' +import { StyleSheet, Text, View } from 'react-native' import { Flow } from 'react-native-animated-spinkit' -import { State, TapGestureHandler } from 'react-native-gesture-handler' +import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' export interface Props { iconFront?: any diff --git a/src/i18n/zh-Hans/screens/meSettingsPush.ts b/src/i18n/zh-Hans/screens/meSettingsPush.ts index 498e5684..ff4a0988 100644 --- a/src/i18n/zh-Hans/screens/meSettingsPush.ts +++ b/src/i18n/zh-Hans/screens/meSettingsPush.ts @@ -14,6 +14,9 @@ export default { description: '经由tooot服务器中转的通知消息已被加密,但可以允许tooot服务器解密并转发消息。tooot消息服务器源码开源,且不留存任何日志。' }, + default: { + heading: '默认通知' // Android notification channel name only + }, follow: { heading: '新关注者' }, @@ -24,10 +27,10 @@ export default { heading: '嘟文被转嘟' }, mention: { - heading: '提及你' + heading: '嘟文提及你' }, poll: { - heading: '投票' + heading: '投票更新' }, howitworks: '了解通知消息转发如何工作' }, diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 07c4e2c3..fd962558 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -319,7 +319,7 @@ const ScreenCompose: React.FC = ({ { useEffect(() => { if ( snapToOffsets.length > - (prevOffsets.current ? prevOffsets.current?.length : 0) + (prevOffsets.current ? prevOffsets.current.length : 0) ) { flatListRef.current?.scrollToOffset({ offset: diff --git a/src/screens/Compose/Root/Header.tsx b/src/screens/Compose/Root/Header.tsx index 9a5be6e1..fc79470c 100644 --- a/src/screens/Compose/Root/Header.tsx +++ b/src/screens/Compose/Root/Header.tsx @@ -18,13 +18,11 @@ const ComposeRootHeader: React.FC = () => { return ( <> - {instanceActive !== -1 && - localInstances.length && - localInstances.length > 1 && ( - - - - )} + {instanceActive !== -1 && localInstances.length > 1 && ( + + + + )} {composeState.spoiler.active ? : null} diff --git a/src/screens/Tabs.tsx b/src/screens/Tabs.tsx index 5e45b4bb..68640530 100644 --- a/src/screens/Tabs.tsx +++ b/src/screens/Tabs.tsx @@ -1,25 +1,15 @@ -import apiInstance from '@api/instance' import haptics from '@components/haptics' import Icon from '@components/Icon' -import { displayMessage } from '@components/Message' import { BottomTabNavigationOptions, createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { NavigatorScreenParams } from '@react-navigation/native' -import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { StackScreenProps } from '@react-navigation/stack' import { getPreviousTab } from '@utils/slices/contextsSlice' -import { - getInstanceAccount, - getInstanceActive, - getInstances, - updateInstanceActive -} from '@utils/slices/instancesSlice' +import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' -import * as Notifications from 'expo-notifications' -import { findIndex } from 'lodash' -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { Platform } from 'react-native' import FastImage from 'react-native-fast-image' import { useQueryClient } from 'react-query' @@ -28,6 +18,8 @@ import TabLocal from './Tabs/Local' import TabMe from './Tabs/Me' import TabNotifications from './Tabs/Notifications' import TabPublic from './Tabs/Public' +import pushReceive from './Tabs/utils/pushReceive' +import pushRespond from './Tabs/utils/pushRespond' export type ScreenTabsParamList = { 'Tab-Local': NavigatorScreenParams @@ -42,108 +34,23 @@ export type ScreenTabsProp = StackScreenProps< 'Screen-Tabs' > -const convertNotificationToToot = ( - navigation: StackNavigationProp, - id: Mastodon.Notification['id'] -) => { - apiInstance({ - method: 'get', - url: `notifications/${id}` - }).then(({ body }) => { - // @ts-ignore - navigation.navigate('Tab-Notifications', { - screen: 'Tab-Notifications-Root' - }) - if (body.status) { - // @ts-ignore - navigation.navigate('Tab-Notifications', { - screen: 'Tab-Shared-Toot', - params: { toot: body.status } - }) - } - }) -} - const Tab = createBottomTabNavigator() const ScreenTabs = React.memo( ({ navigation }: ScreenTabsProp) => { - // Push notifications + const { mode, theme } = useTheme() + const queryClient = useQueryClient() + + const dispatch = useDispatch() + const instanceActive = useSelector(getInstanceActive) const instances = useSelector( getInstances, (prev, next) => prev.length === next.length ) - useEffect(() => { - const subscription = Notifications.addNotificationReceivedListener( - notification => { - const queryKey: QueryKeyTimeline = [ - 'Timeline', - { page: 'Notifications' } - ] - queryClient.invalidateQueries(queryKey) - const payloadData = notification.request.content.data as { - notification_id?: string - instanceUrl: string - accountId: string - } - const notificationIndex = findIndex( - instances, - instance => - instance.url === payloadData.instanceUrl && - instance.account.id === payloadData.accountId - ) - if (notificationIndex !== -1 && payloadData.notification_id) { - displayMessage({ - duration: 'long', - message: notification.request.content.title!, - description: notification.request.content.body!, - onPress: () => - convertNotificationToToot( - navigation, - // @ts-ignore Typescript is wrong - payloadData.notification_id - ) - }) - } - } - ) - return () => subscription.remove() - }, [instances]) - useEffect(() => { - const subscription = Notifications.addNotificationResponseReceivedListener( - ({ notification }) => { - const payloadData = notification.request.content.data as { - notification_id?: string - instanceUrl: string - accountId: string - } - - const notificationIndex = findIndex( - instances, - instance => - instance.url === payloadData.instanceUrl && - instance.account.id === payloadData.accountId - ) - if (notificationIndex !== -1) { - dispatch(updateInstanceActive(instances[notificationIndex])) - } - if (payloadData.notification_id) { - convertNotificationToToot(navigation, payloadData.notification_id) - } - } - ) - return () => subscription.remove() - }, [instances]) - - const { mode, theme } = useTheme() - const dispatch = useDispatch() - const instanceActive = useSelector(getInstanceActive) - const localAccount = useSelector( - getInstanceAccount, - (prev, next) => prev?.avatarStatic === next?.avatarStatic - ) + pushReceive({ navigation, queryClient, instances }) + pushRespond({ navigation, queryClient, instances, dispatch }) const screenOptions = useCallback( ({ route }): BottomTabNavigationOptions => ({ @@ -169,7 +76,9 @@ const ScreenTabs = React.memo( case 'Tab-Me': return instanceActive !== -1 ? ( ({ diff --git a/src/screens/Tabs/Me/Push.tsx b/src/screens/Tabs/Me/Push.tsx index 4055f9d8..ce760ba1 100644 --- a/src/screens/Tabs/Me/Push.tsx +++ b/src/screens/Tabs/Me/Push.tsx @@ -17,29 +17,25 @@ import { AppState, Linking } from 'react-native' const ScreenMeSettingsPush: React.FC = () => { const { t } = useTranslation('meSettingsPush') - const [appStateVisible, setAppStateVisible] = useState(AppState.currentState) - useEffect(() => { - AppState.addEventListener('change', state => setAppStateVisible(state)) - - return () => { - AppState.removeEventListener('change', state => setAppStateVisible(state)) - } - }, []) - const [pushEnabled, setPushEnabled] = useState() - const [pushCanAskAgain, setPushCanAskAgain] = useState() - useEffect(() => { - const checkPush = async () => { - const settings = await Notifications.getPermissionsAsync() - layoutAnimation() - setPushEnabled(settings.granted) - setPushCanAskAgain(settings.canAskAgain) - } - checkPush() - }, [appStateVisible]) - const dispatch = useDispatch() const instancePush = useSelector(getInstancePush) + const [pushEnabled, setPushEnabled] = useState() + const [pushCanAskAgain, setPushCanAskAgain] = useState() + const checkPush = async () => { + const settings = await Notifications.getPermissionsAsync() + layoutAnimation() + setPushEnabled(settings.granted) + setPushCanAskAgain(settings.canAskAgain) + } + useEffect(() => { + checkPush() + AppState.addEventListener('change', () => checkPush()) + return () => { + AppState.removeEventListener('change', () => {}) + } + }, []) + const isLoading = instancePush?.global.loading || instancePush?.decode.loading const alerts = useMemo(() => { diff --git a/src/screens/Tabs/Me/Root/Collections.tsx b/src/screens/Tabs/Me/Root/Collections.tsx index b70f9061..ad80ddb9 100644 --- a/src/screens/Tabs/Me/Root/Collections.tsx +++ b/src/screens/Tabs/Me/Root/Collections.tsx @@ -24,6 +24,8 @@ const Collections: React.FC = () => { onPress={() => navigation.navigate('Tab-Me-Lists')} /> ) + } else { + return null } }, [listsQuery.isSuccess, listsQuery.data, i18n.language]) @@ -55,6 +57,8 @@ const Collections: React.FC = () => { } /> ) + } else { + return null } }, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language]) diff --git a/src/screens/Tabs/Me/Settings/App.tsx b/src/screens/Tabs/Me/Settings/App.tsx index 79d0e68b..dead897b 100644 --- a/src/screens/Tabs/Me/Settings/App.tsx +++ b/src/screens/Tabs/Me/Settings/App.tsx @@ -4,9 +4,11 @@ import { MenuContainer, MenuRow } from '@components/Menu' import { useActionSheet } from '@expo/react-native-action-sheet' import { useNavigation } from '@react-navigation/native' import i18n from '@root/i18n/i18n' +import androidDefaults from '@utils/slices/instances/push/androidDefaults' import { getInstanceActive, - getInstancePush + getInstancePush, + getInstances } from '@utils/slices/instancesSlice' import { changeBrowser, @@ -17,8 +19,10 @@ import { getSettingsBrowser } from '@utils/slices/settingsSlice' import { useTheme } from '@utils/styles/ThemeManager' +import * as Notifications from 'expo-notifications' import React from 'react' import { useTranslation } from 'react-i18next' +import { Platform } from 'react-native' import { useDispatch, useSelector } from 'react-redux' const SettingsApp: React.FC = () => { @@ -28,6 +32,7 @@ const SettingsApp: React.FC = () => { const { setTheme } = useTheme() const { t } = useTranslation('meSettings') + const instances = useSelector(getInstances, () => true) const instanceActive = useSelector(getInstanceActive) const settingsLanguage = useSelector(getSettingsLanguage) const settingsTheme = useSelector(getSettingsTheme) @@ -80,9 +85,68 @@ const SettingsApp: React.FC = () => { new: availableLanguages[buttonIndex] }) haptics('Success') + // @ts-ignore dispatch(changeLanguage(availableLanguages[buttonIndex])) i18n.changeLanguage(availableLanguages[buttonIndex]) + + // Update Android notification channel language + if (Platform.OS === 'android') { + instances.forEach(instance => { + const accountFull = `@${instance.account.acct}@${instance.uri}` + if (instance.push.decode.value === false) { + Notifications.setNotificationChannelAsync( + `${accountFull}_default`, + { + groupId: accountFull, + name: t('meSettingsPush:content.default.heading'), + ...androidDefaults + } + ) + } else { + Notifications.setNotificationChannelAsync( + `${accountFull}_follow`, + { + groupId: accountFull, + name: t('meSettingsPush:content.follow.heading'), + ...androidDefaults + } + ) + Notifications.setNotificationChannelAsync( + `${accountFull}_favourite`, + { + groupId: accountFull, + name: t('meSettingsPush:content.favourite.heading'), + ...androidDefaults + } + ) + Notifications.setNotificationChannelAsync( + `${accountFull}_reblog`, + { + groupId: accountFull, + name: t('meSettingsPush:content.reblog.heading'), + ...androidDefaults + } + ) + Notifications.setNotificationChannelAsync( + `${accountFull}_mention`, + { + groupId: accountFull, + name: t('meSettingsPush:content.mention.heading'), + ...androidDefaults + } + ) + Notifications.setNotificationChannelAsync( + `${accountFull}_poll`, + { + groupId: accountFull, + name: t('meSettingsPush:content.poll.heading'), + ...androidDefaults + } + ) + } + }) + } } } ) diff --git a/src/screens/Tabs/Me/Settings/Dev.tsx b/src/screens/Tabs/Me/Settings/Dev.tsx index e99d5f05..97f1bbfb 100644 --- a/src/screens/Tabs/Me/Settings/Dev.tsx +++ b/src/screens/Tabs/Me/Settings/Dev.tsx @@ -25,7 +25,7 @@ const SettingsDev: React.FC = () => { color: theme.primary }} > - {instances[instanceActive].token} + {instances[instanceActive]?.token} { let skipUpdate = true - skipUpdate = prev.account?.id === next.account?.id - skipUpdate = prev.account?.acct === next.account?.acct + if (prev.account?.id !== next.account?.id) { + skipUpdate = false + } + if (prev.account?.acct === next.account?.acct) { + skipUpdate = false + } return skipUpdate }) diff --git a/src/screens/Tabs/Shared/Account/Information/Created.tsx b/src/screens/Tabs/Shared/Account/Information/Created.tsx index bd1efd6d..59068295 100644 --- a/src/screens/Tabs/Shared/Account/Information/Created.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Created.tsx @@ -33,7 +33,7 @@ const AccountInformationCreated: React.FC = ({ account }) => { }} > {t('content.created_at', { - date: new Date(account?.created_at || '').toLocaleDateString( + date: new Date(account.created_at || '').toLocaleDateString( i18n.language, { year: 'numeric', diff --git a/src/screens/Tabs/Shared/Account/Information/Stats.tsx b/src/screens/Tabs/Shared/Account/Information/Stats.tsx index ef33a0e2..8ad58f01 100644 --- a/src/screens/Tabs/Shared/Account/Information/Stats.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Stats.tsx @@ -73,7 +73,7 @@ const AccountInformationStats: React.FC = ({ account, myInfo }) => { { analytics('account_stats_followers_press', { diff --git a/src/screens/Tabs/utils/pushNavigate.ts b/src/screens/Tabs/utils/pushNavigate.ts new file mode 100644 index 00000000..849c309a --- /dev/null +++ b/src/screens/Tabs/utils/pushNavigate.ts @@ -0,0 +1,31 @@ +import apiInstance from '@api/instance' +import { StackNavigationProp } from '@react-navigation/stack' + +const pushNavigate = ( + navigation: StackNavigationProp, + id?: Mastodon.Notification['id'] +) => { + // @ts-ignore + navigation.navigate('Tab-Notifications', { + screen: 'Tab-Notifications-Root' + }) + + if (!id) { + return + } + + apiInstance({ + method: 'get', + url: `notifications/${id}` + }).then(({ body }) => { + if (body.status) { + // @ts-ignore + navigation.navigate('Tab-Notifications', { + screen: 'Tab-Shared-Toot', + params: { toot: body.status } + }) + } + }) +} + +export default pushNavigate diff --git a/src/screens/Tabs/utils/pushReceive.ts b/src/screens/Tabs/utils/pushReceive.ts new file mode 100644 index 00000000..07118bb2 --- /dev/null +++ b/src/screens/Tabs/utils/pushReceive.ts @@ -0,0 +1,58 @@ +import { displayMessage } from '@components/Message' +import { StackNavigationProp } from '@react-navigation/stack' +import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice' +import * as Notifications from 'expo-notifications' +import { findIndex } from 'lodash' +import { useEffect } from 'react' +import { QueryClient } from 'react-query' +import { useDispatch } from 'react-redux' +import pushNavigate from './pushNavigate' + +export interface Params { + navigation: StackNavigationProp + queryClient: QueryClient + instances: Instance[] +} + +const pushReceive = ({ navigation, queryClient, instances }: Params) => { + const dispatch = useDispatch() + + return useEffect(() => { + const subscription = Notifications.addNotificationReceivedListener( + notification => { + const queryKey: QueryKeyTimeline = [ + 'Timeline', + { page: 'Notifications' } + ] + queryClient.invalidateQueries(queryKey) + const payloadData = notification.request.content.data as { + notification_id?: string + instanceUrl: string + accountId: string + } + + const notificationIndex = findIndex( + instances, + instance => + instance.url === payloadData.instanceUrl && + instance.account.id === payloadData.accountId + ) + displayMessage({ + duration: 'long', + message: notification.request.content.title!, + description: notification.request.content.body!, + onPress: () => { + if (notificationIndex !== -1) { + dispatch(updateInstanceActive(instances[notificationIndex])) + } + pushNavigate(navigation, payloadData.notification_id) + } + }) + } + ) + return () => subscription.remove() + }, [instances]) +} + +export default pushReceive diff --git a/src/screens/Tabs/utils/pushRespond.ts b/src/screens/Tabs/utils/pushRespond.ts new file mode 100644 index 00000000..5b1fd1a2 --- /dev/null +++ b/src/screens/Tabs/utils/pushRespond.ts @@ -0,0 +1,54 @@ +import { StackNavigationProp } from '@react-navigation/stack' +import { Dispatch } from '@reduxjs/toolkit' +import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice' +import * as Notifications from 'expo-notifications' +import { findIndex } from 'lodash' +import { useEffect } from 'react' +import { QueryClient } from 'react-query' +import pushNavigate from './pushNavigate' + +export interface Params { + navigation: StackNavigationProp + queryClient: QueryClient + instances: Instance[] + dispatch: Dispatch +} + +const pushRespond = ({ + navigation, + queryClient, + instances, + dispatch +}: Params) => { + return useEffect(() => { + const subscription = Notifications.addNotificationResponseReceivedListener( + ({ notification }) => { + const queryKey: QueryKeyTimeline = [ + 'Timeline', + { page: 'Notifications' } + ] + queryClient.invalidateQueries(queryKey) + const payloadData = notification.request.content.data as { + notification_id?: string + instanceUrl: string + accountId: string + } + + const notificationIndex = findIndex( + instances, + instance => + instance.url === payloadData.instanceUrl && + instance.account.id === payloadData.accountId + ) + if (notificationIndex !== -1) { + dispatch(updateInstanceActive(instances[notificationIndex])) + } + pushNavigate(navigation, payloadData.notification_id) + } + ) + return () => subscription.remove() + }, [instances]) +} + +export default pushRespond diff --git a/src/startup/push.ts b/src/startup/push.ts index f01ecec4..1edccabc 100644 --- a/src/startup/push.ts +++ b/src/startup/push.ts @@ -10,8 +10,6 @@ const push = () => { shouldSetBadge: false }) }) - Notifications.setBadgeCountAsync(0) - Notifications.dismissAllNotificationsAsync() } export default push diff --git a/src/utils/slices/instances/push/androidDefaults.ts b/src/utils/slices/instances/push/androidDefaults.ts new file mode 100644 index 00000000..2d191e05 --- /dev/null +++ b/src/utils/slices/instances/push/androidDefaults.ts @@ -0,0 +1,11 @@ +import * as Notifications from 'expo-notifications' + +const androidDefaults = { + importance: Notifications.AndroidImportance.DEFAULT, + bypassDnd: false, + showBadge: true, + enableLights: true, + enableVibrate: true +} + +export default androidDefaults diff --git a/src/utils/slices/instances/push/register.ts b/src/utils/slices/instances/push/register.ts index 01749429..a0a457ee 100644 --- a/src/utils/slices/instances/push/register.ts +++ b/src/utils/slices/instances/push/register.ts @@ -1,5 +1,6 @@ import apiGeneral from '@api/general' import apiInstance from '@api/instance' +import i18n from '@root/i18n/i18n' import { RootState } from '@root/store' import { getInstance, @@ -8,6 +9,7 @@ import { } from '@utils/slices/instancesSlice' import * as Notifications from 'expo-notifications' import { Platform } from 'react-native' +import androidDefaults from './androidDefaults' const register1 = async ({ expoToken, @@ -66,17 +68,6 @@ const pushRegister = async ( return Promise.reject() } - const { status: existingStatus } = await Notifications.getPermissionsAsync() - let finalStatus = existingStatus - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync() - finalStatus = status - } - if (finalStatus !== 'granted') { - alert('Failed to get push token for push notification!') - return Promise.reject() - } - const accountId = instanceAccount.id const accountFull = `@${instanceAccount.acct}@${instanceUri}` const serverRes = await register1({ @@ -111,25 +102,45 @@ const pushRegister = async ( }) if (Platform.OS === 'android') { - Notifications.setNotificationChannelAsync('follow', { - name: 'Follow', - importance: Notifications.AndroidImportance.DEFAULT - }) - Notifications.setNotificationChannelAsync('favourite', { - name: 'Favourite', - importance: Notifications.AndroidImportance.DEFAULT - }) - Notifications.setNotificationChannelAsync('reblog', { - name: 'Reblog', - importance: Notifications.AndroidImportance.DEFAULT - }) - Notifications.setNotificationChannelAsync('mention', { - name: 'Mention', - importance: Notifications.AndroidImportance.DEFAULT - }) - Notifications.setNotificationChannelAsync('poll', { - name: 'Poll', - importance: Notifications.AndroidImportance.DEFAULT + Notifications.setNotificationChannelGroupAsync(accountFull, { + name: accountFull, + ...androidDefaults + }).then(group => { + if (group) { + if (instancePush.decode.value === false) { + Notifications.setNotificationChannelAsync(`${group.id}_default`, { + groupId: group.id, + name: i18n.t('meSettingsPush:content.default.heading'), + ...androidDefaults + }) + } else { + Notifications.setNotificationChannelAsync(`${group.id}_follow`, { + groupId: group.id, + name: i18n.t('meSettingsPush:content.follow.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync(`${group.id}_favourite`, { + groupId: group.id, + name: i18n.t('meSettingsPush:content.favourite.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync(`${group.id}_reblog`, { + groupId: group.id, + name: i18n.t('meSettingsPush:content.reblog.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync(`${group.id}_mention`, { + groupId: group.id, + name: i18n.t('meSettingsPush:content.mention.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync(`${group.id}_poll`, { + groupId: group.id, + name: i18n.t('meSettingsPush:content.poll.heading'), + ...androidDefaults + }) + } + } }) } diff --git a/src/utils/slices/instances/push/unregister.ts b/src/utils/slices/instances/push/unregister.ts index 90ddd99d..5f19db70 100644 --- a/src/utils/slices/instances/push/unregister.ts +++ b/src/utils/slices/instances/push/unregister.ts @@ -2,9 +2,13 @@ import apiGeneral from '@api/general' import apiInstance from '@api/instance' import { RootState } from '@root/store' import { getInstance, PUSH_SERVER } from '@utils/slices/instancesSlice' +import * as Notifications from 'expo-notifications' +import { Platform } from 'react-native' const pushUnregister = async (state: RootState, expoToken: string) => { const instance = getInstance(state) + const instanceUri = instance?.uri + const instanceAccount = instance?.account if (!instance?.url || !instance.account.id) { return Promise.reject() @@ -26,6 +30,11 @@ const pushUnregister = async (state: RootState, expoToken: string) => { } }) + if (Platform.OS === 'android') { + const accountFull = `@${instanceAccount?.acct}@${instanceUri}` + Notifications.deleteNotificationChannelGroupAsync(accountFull) + } + return } diff --git a/src/utils/slices/instances/updatePushDecode.ts b/src/utils/slices/instances/updatePushDecode.ts index 569ce82f..005cf99c 100644 --- a/src/utils/slices/instances/updatePushDecode.ts +++ b/src/utils/slices/instances/updatePushDecode.ts @@ -1,13 +1,16 @@ import apiGeneral from '@api/general' import { createAsyncThunk } from '@reduxjs/toolkit' +import i18n from '@root/i18n/i18n' import { RootState } from '@root/store' import * as Notifications from 'expo-notifications' +import { Platform } from 'react-native' import { getInstance, Instance, PUSH_SERVER } from '../instancesSlice' +import androidDefaults from './push/androidDefaults' export const updateInstancePushDecode = createAsyncThunk( 'instances/updatePushDecode', async ( - disalbe: boolean, + disable: boolean, { getState } ): Promise => { const state = getState() as RootState @@ -30,10 +33,61 @@ export const updateInstancePushDecode = createAsyncThunk( expoToken, instanceUrl: instance.url, accountId: instance.account.id, - ...(disalbe && { keys: instance.push.keys }) + ...(disable && { keys: instance.push.keys }) } }) - return Promise.resolve(disalbe) + if (Platform.OS === 'android') { + const accountFull = `@${instance.account.acct}@${instance.uri}` + switch (disable) { + case true: + Notifications.deleteNotificationChannelAsync(`${accountFull}_default`) + Notifications.setNotificationChannelAsync(`${accountFull}_follow`, { + groupId: accountFull, + name: i18n.t('meSettingsPush:content.follow.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync( + `${accountFull}_favourite`, + { + groupId: accountFull, + name: i18n.t('meSettingsPush:content.favourite.heading'), + ...androidDefaults + } + ) + Notifications.setNotificationChannelAsync(`${accountFull}_reblog`, { + groupId: accountFull, + name: i18n.t('meSettingsPush:content.reblog.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync(`${accountFull}_mention`, { + groupId: accountFull, + name: i18n.t('meSettingsPush:content.mention.heading'), + ...androidDefaults + }) + Notifications.setNotificationChannelAsync(`${accountFull}_poll`, { + groupId: accountFull, + name: i18n.t('meSettingsPush:content.poll.heading'), + ...androidDefaults + }) + break + case false: + Notifications.setNotificationChannelAsync(`${accountFull}_default`, { + groupId: accountFull, + name: i18n.t('meSettingsPush:content.default.heading'), + ...androidDefaults + }) + Notifications.deleteNotificationChannelAsync(`${accountFull}_follow`) + Notifications.deleteNotificationChannelAsync( + `${accountFull}_favourite` + ) + Notifications.deleteNotificationChannelAsync(`${accountFull}_reblog`) + Notifications.deleteNotificationChannelAsync(`${accountFull}_mention`) + Notifications.deleteNotificationChannelAsync(`${accountFull}_poll`) + break + } + } + + return Promise.resolve(disable) } ) diff --git a/src/utils/slices/instancesSlice.ts b/src/utils/slices/instancesSlice.ts index 6772834b..c571dfcb 100644 --- a/src/utils/slices/instancesSlice.ts +++ b/src/utils/slices/instancesSlice.ts @@ -4,7 +4,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '@root/store' import { ComposeStateDraft } from '@screens/Compose/utils/types' import { findIndex } from 'lodash' -import { Appearance } from 'react-native' import addInstance from './instances/add' import { connectInstancesPush } from './instances/connectPush' import removeInstance from './instances/remove' diff --git a/src/utils/slices/settingsSlice.ts b/src/utils/slices/settingsSlice.ts index 95855ef8..2e03e712 100644 --- a/src/utils/slices/settingsSlice.ts +++ b/src/utils/slices/settingsSlice.ts @@ -1,8 +1,12 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' -import { RootState } from '@root/store' +import i18n from '@root/i18n/i18n' +import { RootState, store } from '@root/store' import * as Analytics from 'expo-firebase-analytics' import * as Localization from 'expo-localization' +import * as Notifications from 'expo-notifications' import { pickBy } from 'lodash' +import androidDefaults from './instances/push/androidDefaults' +import { getInstances } from './instancesSlice' enum availableLanguages { 'zh-Hans',