From 4c8caf0bfd20c614a755a9f0e4ed66267c3a94d7 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:27:24 -0300 Subject: [PATCH 1/3] feat: mobile troubleshoot notifications (#5330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: troubleshoot notification (#5198) * navigation done * create the icon inside roomslistview, navigation to push troubleshot and layout push troubleshoot * custom header * fix the rooms list view header icon * layout done * update the pt-br i18n * tweak on colors * feat: create notification in room view (#5250) * button and simple navigation done, missing master detail * navigation * add withTheme and colors to rightuttons * fix e2e test * feat: add troubleshooting to notifications pages (#5276) * feat: add troubleshooting to notifications pages * fix e2e test * feat: device notification settings (#5277) * iOS go to device notification setting to change the configuration * go to notification settings with android * add notifee * add the reducer and action * saga request done * add the setInAlert action * tweak at name and add focus to dispatch the request * use the foreground inside pushTroubleShoot to request the notification and fix the icon color * add the request at roomslistview didmount * remove the notification modulo from android * add patch * minor tweak * feat: test push notification (#5329) * feat: test push notification * restApi and definition * push.info and change properly the troubleshootingNotification * use the finally at try/catch * minor tweak * alert and push.info just for 6.6 * fix the react-native.config * minor tweaks * minor tweak * push.test as rest api * change the name from inAlertNotification to highlightTroubleshooting * feat: push quota * refactor the percentage state * removed the push quota feature * minor tweaks * update the link to push notification * the notification icon in the room header will appear if notifications are disabled or highlight troubleshoot is true * remove push quota texts * updated some of the push quota texts * chore: rename highlightTroubleshooting * chore: better prop naming * wip * chore: fix function name * chore: fix colors * fix: copy * chore: 💅 * chore: use fork * chore: naming * chore: fix init * chore: naming * chore: naming * Comment CE code * Use put on troubleshooting saga * Add db column * fix: check notification payload * action: organized translations * fix: push init --------- Co-authored-by: GleidsonDaniel Co-authored-by: Diego Mello Co-authored-by: GleidsonDaniel --- app/actions/actionsTypes.ts | 1 + app/actions/troubleshootingNotification.ts | 21 ++++++ .../VideoConferenceBaseContainer.tsx | 2 +- app/definitions/ISubscription.ts | 1 + app/definitions/redux/index.ts | 4 ++ app/definitions/rest/v1/index.ts | 4 +- app/definitions/rest/v1/push.ts | 24 +++++++ app/definitions/rest/v1/pushToken.ts | 12 ---- app/i18n/locales/en.json | 21 ++++++ app/i18n/locales/pt-BR.json | 23 ++++++- app/lib/constants/colors.ts | 3 - app/lib/database/model/Subscription.js | 5 +- app/lib/database/model/migrations.js | 9 +++ app/lib/database/schema/app.js | 5 +- app/lib/methods/getPermissions.ts | 3 +- app/lib/methods/helpers/info.ts | 17 +++-- app/lib/notifications/index.ts | 49 +++++++------- .../backgroundNotificationHandler.ts | 16 +++-- app/lib/services/restApi.ts | 6 ++ app/reducers/index.js | 2 + .../troubleshootingNotification.test.ts | 30 +++++++++ app/reducers/troubleshootingNotification.ts | 32 +++++++++ app/sagas/index.js | 4 +- app/sagas/login.js | 2 + app/sagas/troubleshootingNotification.ts | 54 +++++++++++++++ app/stacks/InsideStack.tsx | 4 ++ app/stacks/MasterDetailStack/index.tsx | 2 + app/stacks/MasterDetailStack/types.ts | 1 + app/stacks/types.ts | 3 + .../NotificationPreferencesView/index.tsx | 23 ++++++- .../components/CommunityEditionPushQuota.tsx | 56 ++++++++++++++++ .../components/CustomListSection.tsx | 52 +++++++++++++++ .../components/DeviceNotificationSettings.tsx | 50 ++++++++++++++ .../components/NotificationDelay.tsx | 25 +++++++ .../components/PushGatewayConnection.tsx | 65 +++++++++++++++++++ app/views/PushTroubleshootView/index.tsx | 49 ++++++++++++++ app/views/RoomView/RightButtons.tsx | 61 +++++++++++++++-- app/views/RoomView/index.tsx | 3 +- app/views/RoomsListView/index.tsx | 28 +++++++- .../UserNotificationPreferencesView/index.tsx | 30 ++++++++- ios/Podfile.lock | 9 +++ react-native.config.js | 5 -- 42 files changed, 737 insertions(+), 79 deletions(-) create mode 100644 app/actions/troubleshootingNotification.ts create mode 100644 app/definitions/rest/v1/push.ts delete mode 100644 app/definitions/rest/v1/pushToken.ts create mode 100644 app/reducers/troubleshootingNotification.test.ts create mode 100644 app/reducers/troubleshootingNotification.ts create mode 100644 app/sagas/troubleshootingNotification.ts create mode 100644 app/views/PushTroubleshootView/components/CommunityEditionPushQuota.tsx create mode 100644 app/views/PushTroubleshootView/components/CustomListSection.tsx create mode 100644 app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx create mode 100644 app/views/PushTroubleshootView/components/NotificationDelay.tsx create mode 100644 app/views/PushTroubleshootView/components/PushGatewayConnection.tsx create mode 100644 app/views/PushTroubleshootView/index.tsx diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index cad9c6c044..ae8485ba03 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -96,5 +96,6 @@ export const VIDEO_CONF = createRequestTypes('VIDEO_CONF', [ 'ACCEPT_CALL', 'SET_CALLING' ]); +export const TROUBLESHOOTING_NOTIFICATION = createRequestTypes('TROUBLESHOOTING_NOTIFICATION', ['INIT', 'SET']); export const SUPPORTED_VERSIONS = createRequestTypes('SUPPORTED_VERSIONS', ['SET']); export const IN_APP_FEEDBACK = createRequestTypes('IN_APP_FEEDBACK', ['SET', 'REMOVE', 'CLEAR']); diff --git a/app/actions/troubleshootingNotification.ts b/app/actions/troubleshootingNotification.ts new file mode 100644 index 0000000000..91ef578f95 --- /dev/null +++ b/app/actions/troubleshootingNotification.ts @@ -0,0 +1,21 @@ +import { Action } from 'redux'; + +import { TROUBLESHOOTING_NOTIFICATION } from './actionsTypes'; +import { ITroubleshootingNotification } from '../reducers/troubleshootingNotification'; + +type TSetTroubleshootingNotification = Action & { payload: Partial }; + +export type TActionTroubleshootingNotification = Action & TSetTroubleshootingNotification; + +export function initTroubleshootingNotification(): Action { + return { + type: TROUBLESHOOTING_NOTIFICATION.INIT + }; +} + +export function setTroubleshootingNotification(payload: Partial): TSetTroubleshootingNotification { + return { + type: TROUBLESHOOTING_NOTIFICATION.SET, + payload + }; +} diff --git a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceBaseContainer.tsx b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceBaseContainer.tsx index c6756727ae..622a4d1800 100644 --- a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceBaseContainer.tsx +++ b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceBaseContainer.tsx @@ -36,7 +36,7 @@ export const VideoConferenceBaseContainer = ({ variant, children }: VideoConfMes }, issue: { icon: 'phone-issue', - color: colors.statusFontOnWarning, + color: colors.statusFontWarning, backgroundColor: colors.statusBackgroundWarning, label: i18n.t('Call_issue') } diff --git a/app/definitions/ISubscription.ts b/app/definitions/ISubscription.ts index 72fd742ba2..9e318cd1ce 100644 --- a/app/definitions/ISubscription.ts +++ b/app/definitions/ISubscription.ts @@ -110,6 +110,7 @@ export interface ISubscription { threads: RelationModified; threadMessages: RelationModified; uploads: RelationModified; + disableNotifications?: boolean; } export type TSubscriptionModel = ISubscription & diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts index 5724c49b9e..9102b9425e 100644 --- a/app/definitions/redux/index.ts +++ b/app/definitions/redux/index.ts @@ -40,6 +40,8 @@ import { IEnterpriseModules } from '../../reducers/enterpriseModules'; import { IVideoConf } from '../../reducers/videoConf'; import { TActionUsersRoles } from '../../actions/usersRoles'; import { TUsersRoles } from '../../reducers/usersRoles'; +import { ITroubleshootingNotification } from '../../reducers/troubleshootingNotification'; +import { TActionTroubleshootingNotification } from '../../actions/troubleshootingNotification'; import { ISupportedVersionsState } from '../../reducers/supportedVersions'; import { IInAppFeedbackState } from '../../reducers/inAppFeedback'; @@ -67,6 +69,7 @@ export interface IApplicationState { roles: IRoles; videoConf: IVideoConf; usersRoles: TUsersRoles; + troubleshootingNotification: ITroubleshootingNotification; supportedVersions: ISupportedVersionsState; inAppFeedback: IInAppFeedbackState; } @@ -90,5 +93,6 @@ export type TApplicationActions = TActionActiveUsers & TActionEnterpriseModules & TActionVideoConf & TActionUsersRoles & + TActionTroubleshootingNotification & TActionSupportedVersions & TInAppFeedbackAction; diff --git a/app/definitions/rest/v1/index.ts b/app/definitions/rest/v1/index.ts index 9fbf3ffc97..d4dcbba0fa 100644 --- a/app/definitions/rest/v1/index.ts +++ b/app/definitions/rest/v1/index.ts @@ -17,7 +17,7 @@ import { E2eEndpoints } from './e2e'; import { SubscriptionsEndpoints } from './subscriptions'; import { VideoConferenceEndpoints } from './videoConference'; import { CommandsEndpoints } from './commands'; -import { PushTokenEndpoints } from './pushToken'; +import { PushEndpoints } from './push'; import { DirectoryEndpoint } from './directory'; import { AutoTranslateEndpoints } from './autotranslate'; import { ModerationEndpoints } from './moderation'; @@ -41,7 +41,7 @@ export type Endpoints = ChannelsEndpoints & SubscriptionsEndpoints & VideoConferenceEndpoints & CommandsEndpoints & - PushTokenEndpoints & + PushEndpoints & DirectoryEndpoint & AutoTranslateEndpoints & ModerationEndpoints; diff --git a/app/definitions/rest/v1/push.ts b/app/definitions/rest/v1/push.ts new file mode 100644 index 0000000000..f00f737bb0 --- /dev/null +++ b/app/definitions/rest/v1/push.ts @@ -0,0 +1,24 @@ +type TPushInfo = { + pushGatewayEnabled: boolean; + defaultPushGateway: boolean; + success: boolean; +}; + +export type PushEndpoints = { + 'push.token': { + POST: (params: { value: string; type: string; appName: string }) => { + result: { + id: string; + token: string; + appName: string; + userId: string; + }; + }; + }; + 'push.info': { + GET: () => TPushInfo; + }; + 'push.test': { + POST: () => { tokensCount: number }; + }; +}; diff --git a/app/definitions/rest/v1/pushToken.ts b/app/definitions/rest/v1/pushToken.ts deleted file mode 100644 index 99726eb2b7..0000000000 --- a/app/definitions/rest/v1/pushToken.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type PushTokenEndpoints = { - 'push.token': { - POST: (params: { value: string; type: string; appName: string }) => { - result: { - id: string; - token: string; - appName: string; - userId: string; - }; - }; - }; -}; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index b4ccbb8e76..f350b1a334 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -24,6 +24,7 @@ "All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages", "All_users_in_the_team_can_write_new_messages": "All users in the team can write new messages", "Allow_Reactions": "Allow reactions", + "Allow_push_notifications_for_rocket_chat": "Allow push notifications for Rocket.Chat", "Also_send_thread_message_to_channel_behavior": "Also send thread message to channel", "Announcement": "Announcement", "App_users_are_not_allowed_to_log_in_directly": "App users are not allowed to log in directly.", @@ -102,6 +103,7 @@ "Code_block": "Code block", "Code_or_password_invalid": "Code or password invalid", "Collaborative": "Collaborative", + "Community_edition_push_quota": "Community push quota", "Condensed": "Condensed", "Confirm": "Confirm", "Confirmation": "Confirmation", @@ -134,6 +136,8 @@ "Create_a_new_workspace": "Create a new workspace", "Create_account": "Create an account", "Created_snippet": "created a snippet", + "Custom_push_gateway_connected_description": "Your workspace uses a custom push notification gateway. Check with your workspace administrator for any issues.", + "Custom_push_gateway_connection": "Custom Gateway Connection", "DELETE": "DELETE", "Dark": "Dark", "Dark_level": "Dark level", @@ -156,6 +160,9 @@ "Description": "Description", "Desktop_Alert_info": "These notifications are delivered in desktop", "Desktop_Notifications": "Desktop notifications", + "Device_notification_settings": "Device notification settings", + "Device_notifications_alert_description": "Please go to your settings app and enable notifications for Rocket.Chat", + "Device_notifications_alert_title": "Notifications disabled", "Direct_Messages": "Direct messages", "Direct_message": "Direct message", "Direct_message_someone": "Direct message someone", @@ -174,6 +181,7 @@ "Do_you_have_a_certificate": "Do you have a certificate?", "Do_you_have_an_account": "Do you have an account?", "Do_you_really_want_to_key_this_room_question_mark": "Do you really want to {{key}} this room?", + "Documentation": "Documentation", "Dont_Have_An_Account": "Don't you have an account?", "Dont_activate": "Don't activate now", "Downloaded_file": "Downloaded file", @@ -387,6 +395,7 @@ "No_channels_in_team": "No Channels on this team", "No_discussions": "No discussions", "No_files": "No files", + "No_further_action_is_needed": "No further action is needed", "No_label_provided": "No {{label}} provided.", "No_limit": "No limit", "No_match_found": "No match found.", @@ -404,6 +413,8 @@ "Nothing": "Nothing", "Nothing_to_save": "Nothing to save!", "Notification_Preferences": "Notification preferences", + "Notification_delay": "Notification delay", + "Notification_delay_description": "There are factors that can contribute to delayed notifications. Learn more in Rocket.Chat's docs.", "Notifications": "Notifications", "Notify_active_in_this_room": "Notify active users in this room", "Notify_all_in_this_room": "Notify all in this room", @@ -461,6 +472,10 @@ "Public": "Public", "Push_Notifications": "Push notifications", "Push_Notifications_Alert_Info": "These notifications are delivered to you when the app is not open", + "Push_Troubleshooting": "Push Troubleshooting", + "Push_gateway_connected_description": "Send a push notification to yourself to check if the gateway is working", + "Push_gateway_connection": "Push Gateway Connection", + "Push_gateway_not_connected_description": "We're not able to connect to the push gateway. If this issue persists please check with your workspace administrator.", "Queued_chats": "Queued chats", "Quote": "Quote", "RESET": "RESET", @@ -605,6 +620,7 @@ "Team_not_found": "Team not found", "Teams": "Teams", "Terms_of_Service": " Terms of service ", + "Test_push_notification": "Test push notification", "The_maximum_number_of_users_has_been_reached": "The maximum number of users has been reached.", "The_room_does_not_exist": "The room does not exist or you may not have access permission", "The_user_will_be_able_to_type_in_roomName": "The user will be able to type in {{roomName}}", @@ -625,6 +641,7 @@ "Token_expired": "Your session has expired. Please log in again.", "Topic": "Topic", "Translate": "Translate", + "Troubleshooting": "Troubleshooting", "Try_again": "Try again", "Two_Factor_Authentication": "Two-factor authentication", "Type_message": "Type message", @@ -690,6 +707,8 @@ "Wi_Fi_and_mobile_data": "Wi-Fi and mobile data", "Without_Servers": "Without workspaces", "Workspace_URL_Example": "Ex. your-company.rocket.chat", + "Workspace_consumption": "Workspace consumption", + "Workspace_consumption_description": "There’s a set amount of push notifications per month", "Workspaces": "Workspaces", "Would_like_to_place_on_hold": "Would you like to place this chat on hold?", "Would_you_like_to_return_the_inquiry": "Would you like to return the inquiry?", @@ -720,6 +739,7 @@ "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Your invite link will expire on {{date}} or after {{usesLeft}} uses.", "Your_invite_link_will_never_expire": "Your invite link will never expire.", "Your_password_is": "Your password is", + "Your_push_was_sent_to_s_devices": "Your push was sent to {{s}} devices", "Your_workspace": "Your workspace", "__count__empty_room_will_be_removed_automatically": "{{count}} empty room will be deleted.", "__count__empty_rooms_will_be_removed_automatically": "{{count}} empty rooms will be deleted.", @@ -761,6 +781,7 @@ "error-invalid-file-type": "Invalid file type", "error-invalid-password": "Invalid password", "error-invalid-room-name": "{{room_name}} is not a valid room name", + "error-no-tokens-for-this-user": "There are no tokens for this user", "error-not-allowed": "Not allowed", "error-not-permission-to-upload-file": "You don't have permission to upload files", "error-save-image": "Error while saving image", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index e6fd6d850d..67e06b1d6b 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -23,6 +23,7 @@ "All_users_in_the_channel_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas", "All_users_in_the_team_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas", "Allow_Reactions": "Permitir reagir", + "Allow_push_notifications_for_rocket_chat": "Nenhuma ação adicional é necessária", "Also_send_thread_message_to_channel_behavior": "Também enviar mensagem do tópico para o canal", "Announcement": "Anúncio", "App_users_are_not_allowed_to_log_in_directly": "Usuários do aplicativo não estão autorizados a fazer login diretamente.", @@ -55,7 +56,7 @@ "Call_issue": "Chamada com problemas", "Call_ongoing": "Chamada em andamento", "Call_rejected": "Chamada rejeitada", - "Call_started": "Chamada iniciada", + "Call_started": "Chamada Iniciada", "Call_was_not_answered": "A chamada não foi atendida", "Calling": "Chamando", "Cancel": "Cancelar", @@ -99,6 +100,7 @@ "Close_emoji_selector": "Fechar seletor de emojis", "Code_or_password_invalid": "Código ou senha inválido", "Collaborative": "Colaborativo", + "Community_edition_push_quota": "Cota de notificações push Community Edition", "Condensed": "Condensado", "Confirm": "Confirmar", "Confirmation": "Confirmação", @@ -131,6 +133,8 @@ "Create_a_new_workspace": "Criar nova área de trabalho", "Create_account": "Criar conta", "Created_snippet": "criou um snippet", + "Custom_push_gateway_connected_description": "Seu workspace utiliza um gateway de notificação push personalizado. Verifique com o administrador do seu workspace se há algum problema.", + "Custom_push_gateway_connection": "Conexão Personalizada com o Gateway", "DELETE": "EXCLUIR", "Dark": "Escuro", "Dark_level": "Nível escuro", @@ -153,6 +157,9 @@ "Description": "Descrição", "Desktop_Alert_info": "Essas notificações são entregues a você na área de trabalho", "Desktop_Notifications": "Notificações da área de trabalho", + "Device_notification_settings": "Configurações de notificações do dispositivo", + "Device_notifications_alert_description": "Por favor, vá para o aplicativo de configurações e habilite as notificações para o Rocket.Chat.", + "Device_notifications_alert_title": "Notificações desativadas", "Direct_Messages": "Mensagens diretas", "Direct_message": "Mensagem direta", "Direct_message_someone": "Enviar mensagem direta para alguém", @@ -171,6 +178,7 @@ "Do_you_have_a_certificate": "Você tem um certificado?", "Do_you_have_an_account": "Você tem uma conta?", "Do_you_really_want_to_key_this_room_question_mark": "Você quer realmente {{key}} esta sala?", + "Documentation": "Documentação", "Dont_Have_An_Account": "Não tem uma conta?", "Dont_activate": "Não ativar agora", "Downloaded_file": "Arquivo baixado", @@ -267,6 +275,7 @@ "Jitsi_authentication_before_making_calls_admin": "Jitsi pode exigir autenticação antes de fazer chamadas. Para saber mais sobre as políticas deles, visite o site do Jitsi. Você também pode atualizar o aplicativo padrão para chamadas de vídeo nas preferências.", "Jitsi_authentication_before_making_calls_ask_admin": "Se você acredita que há problemas com o Jitsi e sua autenticação, peça ajuda a um administrador do espaço de trabalho.", "Jitsi_may_require_authentication": "O Jitsi pode exigir autenticação", + "Jitsi_may_requires_authentication": "Jitsi pode exigir autenticação", "Join": "Entrar", "Join_Code": "Insira o código da sala", "Join_our_open_workspace": "Entrar na nossa workspace pública", @@ -381,6 +390,7 @@ "No_channels_in_team": "Nenhum canal nesta equipe", "No_discussions": "Sem discussões", "No_files": "Não há arquivos", + "No_further_action_is_needed": "Ir para configurações do dispositivo", "No_label_provided": "Sem {{label}}.", "No_limit": "Sem limite", "No_match_found": "Nenhum resultado encontrado.", @@ -397,6 +407,8 @@ "Nothing": "Nada", "Nothing_to_save": "Nada para salvar!", "Notification_Preferences": "Preferências de notificação", + "Notification_delay": "Atraso de notificação", + "Notification_delay_description": "Existem fatores que podem contribuir para atrasos nas notificações. Saiba mais na documentação do Rocket.Chat.", "Notifications": "Notificações", "Notify_active_in_this_room": "Notificar usuários ativos nesta sala", "Notify_all_in_this_room": "Notificar todos nesta sala", @@ -452,6 +464,10 @@ "Public": "Público", "Push_Notifications": "Notificações push", "Push_Notifications_Alert_Info": "Essas notificações são entregues a você quando o aplicativo não está aberto", + "Push_Troubleshooting": "Solucionar Problemas de Push", + "Push_gateway_connected_description": "Envie uma notificação push para si mesmo para verificar se o gateway está funcionando.", + "Push_gateway_connection": "Conexão com o Gateway de Push", + "Push_gateway_not_connected_description": "Não conseguimos conectar ao gateway de push. Se esse problema persistir, por favor, verifique com o administrador do seu workspace.", "Queued_chats": "Bate-papos na fila", "Quote": "Citar", "RESET": "RESETAR", @@ -593,6 +609,7 @@ "Team_not_found": "Time não encontrado", "Teams": "Times", "Terms_of_Service": " Termos de serviço ", + "Test_push_notification": "Testar notificação push", "The_maximum_number_of_users_has_been_reached": "O número máximo de usuários foi atingido.", "The_room_does_not_exist": "A sala não existe ou você pode não ter permissão de acesso", "The_user_will_be_able_to_type_in_roomName": "O usuário poderá digitar em {{roomName}}", @@ -678,6 +695,8 @@ "Wi_Fi_and_mobile_data": "Wi-Fi e dados móveis", "Without_Servers": "Sem workspaces", "Workspace_URL_Example": "Ex. sua-empresa.rocket.chat", + "Workspace_consumption": "Consumo do Workspace", + "Workspace_consumption_description": "Existe uma quantidade definida de notificações push por mês", "Workspaces": "Workspaces", "Would_like_to_place_on_hold": "Gostaria de colocar essa conversa em espera?", "Would_you_like_to_return_the_inquiry": "Deseja retornar a consulta?", @@ -708,6 +727,7 @@ "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Seu link de convite irá vencer em {{date}} ou depois de {{usesLeft}} usos.", "Your_invite_link_will_never_expire": "Seu link de convite nunca irá vencer.", "Your_password_is": "Sua senha é", + "Your_push_was_sent_to_s_devices": "A sua notificação foi enviada para {{s}} dispositivos", "Your_workspace": "Sua workspace", "__count__empty_room_will_be_removed_automatically": "{{count}} sala vazia será excluída.", "__count__empty_rooms_will_be_removed_automatically": "{{count}} salas vazias serão excluídas.", @@ -749,6 +769,7 @@ "error-invalid-file-type": "Tipo de arquivo inválido", "error-invalid-password": "Senha inválida", "error-invalid-room-name": "{{room_name}} não é um nome de sala válido", + "error-no-tokens-for-this-user": "Não existem tokens para este usuário", "error-not-allowed": "Não permitido", "error-not-permission-to-upload-file": "Você não tem permissão para enviar arquivos", "error-save-image": "Erro ao salvar imagem", diff --git a/app/lib/constants/colors.ts b/app/lib/constants/colors.ts index 1b91535ceb..1be9c6add7 100644 --- a/app/lib/constants/colors.ts +++ b/app/lib/constants/colors.ts @@ -287,7 +287,6 @@ export const colors = { gray100: '#CBCED1', n900: '#1F2329', statusBackgroundWarning: '#FFECAD', - statusFontOnWarning: '#B88D00', overlayColor: '#1F2329CC', taskBoxColor: '#9297a2', ...mentions, @@ -369,7 +368,6 @@ export const colors = { gray100: '#CBCED1', n900: '#FFFFFF', statusBackgroundWarning: '#FFECAD', - statusFontOnWarning: '#B88D00', overlayColor: '#1F2329CC', taskBoxColor: '#9297a2', ...mentions, @@ -451,7 +449,6 @@ export const colors = { gray100: '#CBCED1', n900: '#FFFFFF', statusBackgroundWarning: '#FFECAD', - statusFontOnWarning: '#B88D00', overlayColor: '#1F2329CC', taskBoxColor: '#9297a2', ...mentions, diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index 59ade5c9e7..cda8faa8f5 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -145,6 +145,8 @@ export default class Subscription extends Model { @json('source', sanitizer) source; + @field('disable_notifications') disableNotifications; + asPlain() { return { _id: this._id, @@ -207,7 +209,8 @@ export default class Subscription extends Model { teamMain: this.teamMain, onHold: this.onHold, usersCount: this.usersCount, - source: this.source + source: this.source, + disableNotifications: this.disableNotifications }; } } diff --git a/app/lib/database/model/migrations.js b/app/lib/database/model/migrations.js index 08a111178e..37b0b5123c 100644 --- a/app/lib/database/model/migrations.js +++ b/app/lib/database/model/migrations.js @@ -284,6 +284,15 @@ export default schemaMigrations({ columns: [{ name: 'unmuted', type: 'string', isOptional: true }] }) ] + }, + { + toVersion: 24, + steps: [ + addColumns({ + table: 'subscriptions', + columns: [{ name: 'disable_notifications', type: 'boolean', isOptional: true }] + }) + ] } ] }); diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js index 5ae48d6df0..b0a99752bc 100644 --- a/app/lib/database/schema/app.js +++ b/app/lib/database/schema/app.js @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export default appSchema({ - version: 23, + version: 24, tables: [ tableSchema({ name: 'subscriptions', @@ -66,7 +66,8 @@ export default appSchema({ { name: 'source', type: 'string', isOptional: true }, { name: 'hide_mention_status', type: 'boolean', isOptional: true }, { name: 'users_count', type: 'number', isOptional: true }, - { name: 'unmuted', type: 'string', isOptional: true } + { name: 'unmuted', type: 'string', isOptional: true }, + { name: 'disable_notifications', type: 'boolean', isOptional: true } ] }), tableSchema({ diff --git a/app/lib/methods/getPermissions.ts b/app/lib/methods/getPermissions.ts index cd741ee620..f79de0121d 100644 --- a/app/lib/methods/getPermissions.ts +++ b/app/lib/methods/getPermissions.ts @@ -60,7 +60,8 @@ export const SUPPORTED_PERMISSIONS = [ 'view-canned-responses', 'mobile-upload-file', 'delete-own-message', - 'call-management' + 'call-management', + 'test-push-notifications' ] as const; export async function setPermissions(): Promise { diff --git a/app/lib/methods/helpers/info.ts b/app/lib/methods/helpers/info.ts index 2fc903d04e..287a06b182 100644 --- a/app/lib/methods/helpers/info.ts +++ b/app/lib/methods/helpers/info.ts @@ -5,12 +5,17 @@ import I18n from '../../../i18n'; export const showErrorAlert = (message: string, title?: string, onPress = () => {}): void => Alert.alert(title || '', message, [{ text: 'OK', onPress }], { cancelable: true }); -export const showErrorAlertWithEMessage = (e: any): void => { - const messageError = - e.data && e.data.error.includes('[error-too-many-requests]') - ? I18n.t('error-too-many-requests', { seconds: e.data.error.replace(/\D/g, '') }) - : e.data.errorType; - showErrorAlert(messageError); +export const showErrorAlertWithEMessage = (e: any, title?: string): void => { + let errorMessage: string = e?.data?.error; + + if (errorMessage.includes('[error-too-many-requests]')) { + const seconds = errorMessage.replace(/\D/g, ''); + errorMessage = I18n.t('error-too-many-requests', { seconds }); + } else { + errorMessage = I18n.isTranslated(errorMessage) ? I18n.t(errorMessage) : errorMessage; + } + + showErrorAlert(errorMessage, title); }; interface IShowConfirmationAlert { diff --git a/app/lib/notifications/index.ts b/app/lib/notifications/index.ts index edc6e0440a..f477b6474b 100644 --- a/app/lib/notifications/index.ts +++ b/app/lib/notifications/index.ts @@ -1,5 +1,6 @@ import EJSON from 'ejson'; +import { appInit } from '../../actions/app'; import { deepLinkingClickCallPush, deepLinkingOpen } from '../../actions/deepLinking'; import { INotification, SubscriptionType } from '../../definitions'; import { isFDroidBuild } from '../constants'; @@ -18,39 +19,43 @@ interface IEjson { export const onNotification = (push: INotification): void => { const identifier = String(push?.payload?.action?.identifier); if (identifier === 'ACCEPT_ACTION' || identifier === 'DECLINE_ACTION') { - if (push.payload) { - const notification = EJSON.parse(push.payload.ejson); + if (push?.payload && push?.payload?.ejson) { + const notification = EJSON.parse(push?.payload?.ejson); store.dispatch(deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' })); return; } } - if (push.payload) { + if (push?.payload) { try { - const notification = push.payload; - const { rid, name, sender, type, host, messageId }: IEjson = EJSON.parse(notification.ejson); + const notification = push?.payload; + if (notification.ejson) { + const { rid, name, sender, type, host, messageId }: IEjson = EJSON.parse(notification.ejson); - const types: Record = { - c: 'channel', - d: 'direct', - p: 'group', - l: 'channels' - }; - let roomName = type === SubscriptionType.DIRECT ? sender.username : name; - if (type === SubscriptionType.OMNICHANNEL) { - roomName = sender.name; - } + const types: Record = { + c: 'channel', + d: 'direct', + p: 'group', + l: 'channels' + }; + let roomName = type === SubscriptionType.DIRECT ? sender.username : name; + if (type === SubscriptionType.OMNICHANNEL) { + roomName = sender.name; + } - const params = { - host, - rid, - messageId, - path: `${types[type]}/${roomName}` - }; - store.dispatch(deepLinkingOpen(params)); + const params = { + host, + rid, + messageId, + path: `${types[type]}/${roomName}` + }; + store.dispatch(deepLinkingOpen(params)); + return; + } } catch (e) { console.warn(e); } } + store.dispatch(appInit()); }; export const getDeviceToken = (): string => deviceToken; diff --git a/app/lib/notifications/videoConf/backgroundNotificationHandler.ts b/app/lib/notifications/videoConf/backgroundNotificationHandler.ts index 6d685c2f97..ab98808b34 100644 --- a/app/lib/notifications/videoConf/backgroundNotificationHandler.ts +++ b/app/lib/notifications/videoConf/backgroundNotificationHandler.ts @@ -105,13 +105,15 @@ const displayVideoConferenceNotification = async (notification: NotificationData const setBackgroundNotificationHandler = () => { createChannel(); messaging().setBackgroundMessageHandler(async message => { - const notification: NotificationData = ejson.parse(message?.data?.ejson as string); - if (notification?.notificationType === VIDEO_CONF_TYPE) { - if (notification.status === 0) { - await displayVideoConferenceNotification(notification); - } else if (notification.status === 4) { - const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, ''); - await notifee.cancelNotification(id); + if (message?.data?.ejson) { + const notification: NotificationData = ejson.parse(message?.data?.ejson as string); + if (notification?.notificationType === VIDEO_CONF_TYPE) { + if (notification.status === 0) { + await displayVideoConferenceNotification(notification); + } else if (notification.status === 4) { + const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, ''); + await notifee.cancelNotification(id); + } } } diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index b1e673381e..47c3675088 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -905,6 +905,12 @@ export const removePushToken = (): Promise => { return Promise.resolve(); }; +// RC 6.6.0 +export const pushTest = () => sdk.post('push.test'); + +// RC 6.5.0 +export const pushInfo = () => sdk.get('push.info'); + export const sendEmailCode = () => { const { username } = reduxStore.getState().login.user as IUser; // RC 3.1.0 diff --git a/app/reducers/index.js b/app/reducers/index.js index cdc02f3926..4258685f94 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -23,6 +23,7 @@ import permissions from './permissions'; import roles from './roles'; import videoConf from './videoConf'; import usersRoles from './usersRoles'; +import troubleshootingNotification from './troubleshootingNotification'; import supportedVersions from './supportedVersions'; import inAppFeedback from './inAppFeedback'; @@ -50,6 +51,7 @@ export default combineReducers({ roles, videoConf, usersRoles, + troubleshootingNotification, supportedVersions, inAppFeedback }); diff --git a/app/reducers/troubleshootingNotification.test.ts b/app/reducers/troubleshootingNotification.test.ts new file mode 100644 index 0000000000..2e6254e949 --- /dev/null +++ b/app/reducers/troubleshootingNotification.test.ts @@ -0,0 +1,30 @@ +import { setTroubleshootingNotification, initTroubleshootingNotification } from '../actions/troubleshootingNotification'; +import { mockedStore } from './mockedStore'; +import { ITroubleshootingNotification, initialState } from './troubleshootingNotification'; + +describe('test troubleshootingNotification reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().troubleshootingNotification; + expect(state).toEqual(initialState); + }); + + it('should return correctly the value after call initTroubleshootingNotification action', () => { + mockedStore.dispatch(initTroubleshootingNotification()); + const state = mockedStore.getState().troubleshootingNotification; + expect(state).toEqual(initialState); + }); + + it('should return correctly value after call troubleshootingNotification action', () => { + const payload: ITroubleshootingNotification = { + deviceNotificationEnabled: true, + issuesWithNotifications: false, + defaultPushGateway: true, + pushGatewayEnabled: true, + consumptionPercentage: 0, + isCommunityEdition: false + }; + mockedStore.dispatch(setTroubleshootingNotification(payload)); + const state = mockedStore.getState().troubleshootingNotification; + expect(state).toEqual(payload); + }); +}); diff --git a/app/reducers/troubleshootingNotification.ts b/app/reducers/troubleshootingNotification.ts new file mode 100644 index 0000000000..05fdda0cc1 --- /dev/null +++ b/app/reducers/troubleshootingNotification.ts @@ -0,0 +1,32 @@ +import { TROUBLESHOOTING_NOTIFICATION } from '../actions/actionsTypes'; +import { TActionTroubleshootingNotification } from '../actions/troubleshootingNotification'; + +export interface ITroubleshootingNotification { + deviceNotificationEnabled: boolean; + pushGatewayEnabled: boolean; + defaultPushGateway: boolean; + issuesWithNotifications: boolean; + consumptionPercentage: number; + isCommunityEdition: boolean; +} + +export const initialState: ITroubleshootingNotification = { + deviceNotificationEnabled: false, + pushGatewayEnabled: false, + defaultPushGateway: false, + issuesWithNotifications: false, + consumptionPercentage: 0, + isCommunityEdition: false +}; + +export default (state = initialState, action: TActionTroubleshootingNotification): ITroubleshootingNotification => { + switch (action.type) { + case TROUBLESHOOTING_NOTIFICATION.SET: + return { + ...state, + ...action.payload + }; + default: + return state; + } +}; diff --git a/app/sagas/index.js b/app/sagas/index.js index 12d2ba83f7..b60cc21183 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -14,6 +14,7 @@ import inviteLinks from './inviteLinks'; import createDiscussion from './createDiscussion'; import encryption from './encryption'; import videoConf from './videoConf'; +import troubleshootingNotification from './troubleshootingNotification'; const root = function* root() { yield all([ @@ -30,7 +31,8 @@ const root = function* root() { createDiscussion(), inquiry(), encryption(), - videoConf() + videoConf(), + troubleshootingNotification() ]); }; diff --git a/app/sagas/login.js b/app/sagas/login.js index 2333269912..3dd84034b0 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -17,6 +17,7 @@ import { inviteLinksRequest } from '../actions/inviteLinks'; import { showErrorAlert } from '../lib/methods/helpers/info'; import { localAuthenticate } from '../lib/methods/helpers/localAuthentication'; import { encryptionInit, encryptionStop } from '../actions/encryption'; +import { initTroubleshootingNotification } from '../actions/troubleshootingNotification'; import UserPreferences from '../lib/methods/userPreferences'; import { inquiryRequest, inquiryReset } from '../ee/omnichannel/actions/inquiry'; import { isOmnichannelStatusAvailable } from '../ee/omnichannel/lib'; @@ -236,6 +237,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { yield put(inviteLinksRequest(inviteLinkToken)); } yield showSupportedVersionsWarning(server); + yield put(initTroubleshootingNotification()); } catch (e) { log(e); } diff --git a/app/sagas/troubleshootingNotification.ts b/app/sagas/troubleshootingNotification.ts new file mode 100644 index 0000000000..7710cd5177 --- /dev/null +++ b/app/sagas/troubleshootingNotification.ts @@ -0,0 +1,54 @@ +import { Action } from 'redux'; +import { call, takeLatest, put } from 'typed-redux-saga'; +import notifee, { AuthorizationStatus } from '@notifee/react-native'; + +import { TROUBLESHOOTING_NOTIFICATION } from '../actions/actionsTypes'; +import { setTroubleshootingNotification } from '../actions/troubleshootingNotification'; +import { pushInfo } from '../lib/services/restApi'; +import log from '../lib/methods/helpers/log'; +import { appSelector } from '../lib/hooks'; +import { compareServerVersion } from '../lib/methods/helpers'; + +interface IGenericAction extends Action { + type: string; +} + +function* init() { + const serverVersion = yield* appSelector(state => state.server.version); + let deviceNotificationEnabled = false; + let defaultPushGateway = false; + let pushGatewayEnabled = false; + try { + const { authorizationStatus } = yield* call(notifee.getNotificationSettings); + deviceNotificationEnabled = authorizationStatus > AuthorizationStatus.DENIED; + } catch (e) { + log(e); + } + + try { + if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '6.5.0')) { + const pushInfoResult = yield* call(pushInfo); + if (pushInfoResult.success) { + pushGatewayEnabled = pushInfoResult.pushGatewayEnabled; + defaultPushGateway = pushInfoResult.defaultPushGateway; + } + } + } catch (e) { + log(e); + } + + const issuesWithNotifications = + !deviceNotificationEnabled || (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '6.6.0') && !pushGatewayEnabled); + yield put( + setTroubleshootingNotification({ + deviceNotificationEnabled, + defaultPushGateway, + pushGatewayEnabled, + issuesWithNotifications + }) + ); +} + +export default function* root(): Generator { + yield takeLatest(TROUBLESHOOTING_NOTIFICATION.INIT, init); +} diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index bcf1e071de..1d79840766 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -42,6 +42,7 @@ import DisplayPrefsView from '../views/DisplayPrefsView'; // Settings Stack import SettingsView from '../views/SettingsView'; import SecurityPrivacyView from '../views/SecurityPrivacyView'; +import PushTroubleshootView from '../views/PushTroubleshootView'; import E2EEncryptionSecurityView from '../views/E2EEncryptionSecurityView'; import LanguageView from '../views/LanguageView'; import ThemeView from '../views/ThemeView'; @@ -118,6 +119,7 @@ const ChatsStackNavigator = () => { + @@ -155,6 +157,7 @@ const ProfileStackNavigator = () => { + ); @@ -171,6 +174,7 @@ const SettingsStackNavigator = () => { > + diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx index 00490ea9f6..49aa3baae5 100644 --- a/app/stacks/MasterDetailStack/index.tsx +++ b/app/stacks/MasterDetailStack/index.tsx @@ -27,6 +27,7 @@ import MessagesView from '../../views/MessagesView'; import AutoTranslateView from '../../views/AutoTranslateView'; import DirectoryView from '../../views/DirectoryView'; import NotificationPrefView from '../../views/NotificationPreferencesView'; +import PushTroubleshootView from '../../views/PushTroubleshootView'; import ForwardLivechatView from '../../views/ForwardLivechatView'; import ForwardMessageView from '../../views/ForwardMessageView'; import CloseLivechatView from '../../views/CloseLivechatView'; @@ -187,6 +188,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => { + diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts index 816e4b250a..f6d5c41a05 100644 --- a/app/stacks/MasterDetailStack/types.ts +++ b/app/stacks/MasterDetailStack/types.ts @@ -196,6 +196,7 @@ export type ModalStackParamList = { SecurityPrivacyView: undefined; MediaAutoDownloadView: undefined; E2EEncryptionSecurityView: undefined; + PushTroubleshootView: undefined; SupportedVersionsWarning: { showCloseButton?: boolean; }; diff --git a/app/stacks/types.ts b/app/stacks/types.ts index 8df75b9765..691c54cff1 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -122,6 +122,7 @@ export type ChatsStackParamList = { rid: string; room: TSubscriptionModel; }; + PushTroubleshootView: undefined; CloseLivechatView: { rid: string; departmentId?: string; @@ -188,6 +189,7 @@ export type ProfileStackParamList = { ProfileView: undefined; UserPreferencesView: undefined; UserNotificationPrefView: undefined; + PushTroubleshootView: undefined; ChangeAvatarView: { context: TChangeAvatarViewContext; titleHeader?: string; @@ -207,6 +209,7 @@ export type SettingsStackParamList = { ProfileView: undefined; DisplayPrefsView: undefined; MediaAutoDownloadView: undefined; + PushTroubleshootView: undefined; }; export type AdminPanelStackParamList = { diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index 176b81c1e4..708f0943a1 100644 --- a/app/views/NotificationPreferencesView/index.tsx +++ b/app/views/NotificationPreferencesView/index.tsx @@ -1,6 +1,7 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/core'; import React, { useEffect, useState } from 'react'; import { Switch, Text } from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet'; import { CustomIcon } from '../../containers/CustomIcon'; @@ -91,8 +92,11 @@ const RenderSwitch = ({ preference, room, onChangeValue }: IBaseParams) => { const NotificationPreferencesView = (): React.ReactElement => { const route = useRoute>(); const { rid, room } = route.params; - const navigation = useNavigation(); - const serverVersion = useAppSelector(state => state.server.version); + const navigation = useNavigation>(); + const { serverVersion, isMasterDetail } = useAppSelector(state => ({ + serverVersion: state.server.version, + isMasterDetail: state.app.isMasterDetail + })); const [hideUnreadStatus, setHideUnreadStatus] = useState(room.hideUnreadStatus); useEffect(() => { @@ -108,6 +112,14 @@ const NotificationPreferencesView = (): React.ReactElement => { }); }, []); + const navigateToPushTroubleshootView = () => { + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { screen: 'PushTroubleshootView' }); + } else { + navigation.navigate('PushTroubleshootView'); + } + }; + const saveNotificationSettings = async (key: TUnionOptionsRoomNotifications, params: IRoomNotifications, onError: Function) => { try { // @ts-ignore @@ -202,6 +214,13 @@ const NotificationPreferencesView = (): React.ReactElement => { onChangeValue={saveNotificationSettings} /> + + diff --git a/app/views/PushTroubleshootView/components/CommunityEditionPushQuota.tsx b/app/views/PushTroubleshootView/components/CommunityEditionPushQuota.tsx new file mode 100644 index 0000000000..f513e52dc3 --- /dev/null +++ b/app/views/PushTroubleshootView/components/CommunityEditionPushQuota.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Alert, StyleSheet, Text } from 'react-native'; + +import * as List from '../../../containers/List'; +import i18n from '../../../i18n'; +import { useAppSelector } from '../../../lib/hooks'; +import { useTheme } from '../../../theme'; +import sharedStyles from '../../Styles'; + +const WARNING_MINIMUM_VALUE = 70; +const WARNING_MAXIMUM_VALUE = 90; + +export default function CommunityEditionPushQuota(): React.ReactElement | null { + const { colors } = useTheme(); + const { consumptionPercentage, isCommunityEdition } = useAppSelector(state => ({ + isCommunityEdition: state.troubleshootingNotification.isCommunityEdition, + consumptionPercentage: state.troubleshootingNotification.consumptionPercentage + })); + + if (!isCommunityEdition) return null; + + const percentage = `${Math.floor(consumptionPercentage)}%`; + + let percentageColor = colors.statusFontSuccess; + if (consumptionPercentage > WARNING_MINIMUM_VALUE && consumptionPercentage < WARNING_MAXIMUM_VALUE) { + percentageColor = colors.statusFontWarning; + } + if (consumptionPercentage >= WARNING_MAXIMUM_VALUE) { + percentageColor = colors.statusFontDanger; + } + + const alertWorkspaceConsumption = () => { + Alert.alert(i18n.t('Push_consumption_alert_title'), i18n.t('Push_consumption_alert_description')); + }; + + return ( + + + {percentage}} + /> + + + + ); +} + +const styles = StyleSheet.create({ + pickerText: { + ...sharedStyles.textRegular, + fontSize: 16 + } +}); diff --git a/app/views/PushTroubleshootView/components/CustomListSection.tsx b/app/views/PushTroubleshootView/components/CustomListSection.tsx new file mode 100644 index 0000000000..8bf02c3f9d --- /dev/null +++ b/app/views/PushTroubleshootView/components/CustomListSection.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { Header } from '../../../containers/List'; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16 + }, + headerContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }, + statusContainer: { + width: 10, + height: 10, + borderRadius: 5, + marginRight: 12 + } +}); + +interface ICustomListSection { + children: (React.ReactElement | null)[] | React.ReactElement | null; + title: string; + translateTitle?: boolean; + statusColor?: string; +} + +const CustomHeader = ({ + title, + translateTitle, + statusColor +}: { + title: string; + translateTitle?: boolean; + statusColor?: string; +}) => ( + +
+ {statusColor ? : null} + +); + +const CustomListSection = ({ children, title, translateTitle, statusColor }: ICustomListSection) => ( + + {title ? : null} + {children} + +); + +export default CustomListSection; diff --git a/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx b/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx new file mode 100644 index 0000000000..6cf93c898d --- /dev/null +++ b/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx @@ -0,0 +1,50 @@ +import notifee from '@notifee/react-native'; +import React from 'react'; +import { Linking } from 'react-native'; + +import * as List from '../../../containers/List'; +import i18n from '../../../i18n'; +import { useAppSelector } from '../../../lib/hooks'; +import { isIOS, showErrorAlert } from '../../../lib/methods/helpers'; +import { useTheme } from '../../../theme'; +import CustomListSection from './CustomListSection'; + +export default function DeviceNotificationSettings(): React.ReactElement { + const { colors } = useTheme(); + const { deviceNotificationEnabled } = useAppSelector(state => ({ + deviceNotificationEnabled: state.troubleshootingNotification.deviceNotificationEnabled + })); + + const goToNotificationSettings = () => { + if (isIOS) { + Linking.openURL('app-settings:'); + } else { + notifee.openNotificationSettings(); + } + }; + + const alertDeviceNotificationSettings = () => { + if (deviceNotificationEnabled) return; + showErrorAlert( + i18n.t('Device_notifications_alert_description'), + i18n.t('Device_notifications_alert_title'), + goToNotificationSettings + ); + }; + + return ( + + + + + + ); +} diff --git a/app/views/PushTroubleshootView/components/NotificationDelay.tsx b/app/views/PushTroubleshootView/components/NotificationDelay.tsx new file mode 100644 index 0000000000..5b32a412db --- /dev/null +++ b/app/views/PushTroubleshootView/components/NotificationDelay.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Linking } from 'react-native'; + +import * as List from '../../../containers/List'; +import { useTheme } from '../../../theme'; + +export default function NotificationDelay(): React.ReactElement { + const { colors } = useTheme(); + + const openNotificationDocumentation = () => Linking.openURL('https://go.rocket.chat/i/push-notifications'); + + return ( + + + } + testID='push-troubleshoot-view-notification-delay' + /> + + + + ); +} diff --git a/app/views/PushTroubleshootView/components/PushGatewayConnection.tsx b/app/views/PushTroubleshootView/components/PushGatewayConnection.tsx new file mode 100644 index 0000000000..a0f10396a2 --- /dev/null +++ b/app/views/PushTroubleshootView/components/PushGatewayConnection.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import { Alert } from 'react-native'; + +import * as List from '../../../containers/List'; +import i18n from '../../../i18n'; +import { useAppSelector, usePermissions } from '../../../lib/hooks'; +import { compareServerVersion, showErrorAlertWithEMessage } from '../../../lib/methods/helpers'; +import { Services } from '../../../lib/services'; +import { useTheme } from '../../../theme'; +import CustomListSection from './CustomListSection'; + +export default function PushGatewayConnection(): React.ReactElement | null { + const [loading, setLoading] = useState(false); + const { colors } = useTheme(); + const [testPushNotificationsPermission] = usePermissions(['test-push-notifications']); + const { defaultPushGateway, pushGatewayEnabled, serverVersion } = useAppSelector(state => ({ + pushGatewayEnabled: state.troubleshootingNotification.pushGatewayEnabled, + defaultPushGateway: state.troubleshootingNotification.defaultPushGateway, + foreground: state.app.foreground, + serverVersion: state.server.version + })); + + if (!compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '6.6.0')) return null; + + const handleTestPushNotification = async () => { + setLoading(true); + try { + const result = await Services.pushTest(); + if (result.success) { + Alert.alert(i18n.t('Test_push_notification'), i18n.t('Your_push_was_sent_to_s_devices', { s: result.tokensCount })); + } + } catch (error: any) { + showErrorAlertWithEMessage(error, i18n.t('Test_push_notification')); + } + setLoading(false); + }; + + let infoColor = 'Push_gateway_not_connected_description'; + let statusColor = colors.userPresenceBusy; + if (pushGatewayEnabled) { + statusColor = colors.userPresenceOnline; + infoColor = 'Push_gateway_connected_description'; + } + if (pushGatewayEnabled && !defaultPushGateway) { + statusColor = colors.badgeBackgroundLevel3; + infoColor = 'Custom_push_gateway_connected_description'; + } + + return ( + + + + + + + ); +} diff --git a/app/views/PushTroubleshootView/index.tsx b/app/views/PushTroubleshootView/index.tsx new file mode 100644 index 0000000000..a62cf2d6b3 --- /dev/null +++ b/app/views/PushTroubleshootView/index.tsx @@ -0,0 +1,49 @@ +import { useFocusEffect } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { initTroubleshootingNotification } from '../../actions/troubleshootingNotification'; +import * as List from '../../containers/List'; +import SafeAreaView from '../../containers/SafeAreaView'; +import StatusBar from '../../containers/StatusBar'; +import I18n from '../../i18n'; +import { SettingsStackParamList } from '../../stacks/types'; +// import CommunityEditionPushQuota from './components/CommunityEditionPushQuota'; +import DeviceNotificationSettings from './components/DeviceNotificationSettings'; +import NotificationDelay from './components/NotificationDelay'; +import PushGatewayConnection from './components/PushGatewayConnection'; + +interface IPushTroubleshootViewProps { + navigation: StackNavigationProp; +} + +const PushTroubleshootView = ({ navigation }: IPushTroubleshootViewProps): JSX.Element => { + const dispatch = useDispatch(); + + useFocusEffect( + useCallback(() => { + dispatch(initTroubleshootingNotification()); + }, []) + ); + + useEffect(() => { + navigation.setOptions({ + title: I18n.t('Push_Troubleshooting') + }); + }, [navigation]); + + return ( + + + + + {/* */} + + + + + ); +}; + +export default PushTroubleshootView; diff --git a/app/views/RoomView/RightButtons.tsx b/app/views/RoomView/RightButtons.tsx index b1e724a3e7..50bec628e3 100644 --- a/app/views/RoomView/RightButtons.tsx +++ b/app/views/RoomView/RightButtons.tsx @@ -21,6 +21,7 @@ import { getUserSelector } from '../../selectors/login'; import { TNavigation } from '../../stacks/stackType'; import { ChatsStackParamList } from '../../stacks/types'; import HeaderCallButton from './components/HeaderCallButton'; +import { TColors, TSupportedThemes, withTheme } from '../../theme'; interface IRightButtonsProps extends Pick { userId?: string; @@ -43,6 +44,10 @@ interface IRightButtonsProps extends Pick { showActionSheet: Function; departmentId?: string; rid?: string; + theme?: TSupportedThemes; + colors?: TColors; + issuesWithNotifications: boolean; + notificationsDisabled?: boolean; } interface IRigthButtonsState { @@ -55,6 +60,7 @@ interface IRigthButtonsState { class RightButtonsContainer extends Component { private threadSubscription?: Subscription; private subSubscription?: Subscription; + private room?: TSubscriptionModel; constructor(props: IRightButtonsProps) { super(props); @@ -80,8 +86,8 @@ class RightButtonsContainer extends Component { + const { room } = this; + const { rid, navigation, isMasterDetail, issuesWithNotifications } = this.props; + + if (!rid || !room) { + return; + } + if (!issuesWithNotifications && room) { + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { + screen: 'NotificationPrefView', + params: { rid, room } + }); + } else { + navigation.navigate('NotificationPrefView', { rid, room }); + } + } else if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { + screen: 'PushTroubleshootView' + }); + } else { + navigation.navigate('PushTroubleshootView'); + } + }; + goSearchView = () => { logEvent(events.ROOM_GO_SEARCH); const { rid, t, navigation, isMasterDetail, encrypted } = this.props; @@ -321,7 +361,7 @@ class RightButtonsContainer extends Component + {issuesWithNotifications || notificationsDisabled ? ( + + ) : null} {rid ? : null} {threadsEnabled ? ( ({ userId: getUserSelector(state).id, threadsEnabled: state.settings.Threads_enabled as boolean, isMasterDetail: state.app.isMasterDetail, - livechatRequestComment: state.settings.Livechat_request_comment_when_closing_conversation as boolean + livechatRequestComment: state.settings.Livechat_request_comment_when_closing_conversation as boolean, + issuesWithNotifications: state.troubleshootingNotification.issuesWithNotifications }); -export default connect(mapStateToProps)(RightButtonsContainer); +export default connect(mapStateToProps)(withTheme(RightButtonsContainer)); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 8c719365c4..f446d836e6 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -456,7 +456,7 @@ class RoomView extends React.Component { const t = room?.t; const teamMain = 'teamMain' in room ? room?.teamMain : false; const omnichannelPermissions = { canForwardGuest, canReturnQueue, canPlaceLivechatOnHold }; - + const iSubRoom = room as ISubscription; navigation.setOptions({ headerShown: true, headerTitleAlign: 'left', @@ -513,6 +513,7 @@ class RoomView extends React.Component { toggleFollowThread={this.toggleFollowThread} showActionSheet={this.showActionSheet} departmentId={departmentId} + notificationsDisabled={iSubRoom?.disableNotifications} /> ) }); diff --git a/app/views/RoomsListView/index.tsx b/app/views/RoomsListView/index.tsx index 5f6ebc239d..c5b2639d60 100644 --- a/app/views/RoomsListView/index.tsx +++ b/app/views/RoomsListView/index.tsx @@ -92,6 +92,7 @@ interface IRoomsListViewProps { createPrivateChannelPermission: []; createDiscussionPermission: []; serverVersion: string; + issuesWithNotifications: boolean; } interface IRoomsListViewState { @@ -146,6 +147,7 @@ const shouldUpdateProps = [ 'createPublicChannelPermission', 'createPrivateChannelPermission', 'createDiscussionPermission', + 'issuesWithNotifications', 'supportedVersionsStatus' ]; @@ -200,7 +202,6 @@ class RoomsListView extends React.Component { this.animated = true; // Check if there were changes with sort preference, then call getSubscription to remount the list @@ -334,6 +335,7 @@ class RoomsListView extends React.Component { const { searching, canCreateRoom } = this.state; - const { navigation, isMasterDetail, notificationPresenceCap, supportedVersionsStatus, theme } = this.props; + const { navigation, isMasterDetail, notificationPresenceCap, issuesWithNotifications, supportedVersionsStatus, theme } = + this.props; if (searching) { return { headerTitleAlign: 'left', @@ -458,6 +462,14 @@ class RoomsListView extends React.Component , headerRight: () => ( + {issuesWithNotifications ? ( + + ) : null} {canCreateRoom ? ( { + const { navigation, isMasterDetail } = this.props; + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { screen: 'PushTroubleshootView' }); + } else { + navigation.navigate('PushTroubleshootView'); + } + }; + goQueue = () => { logEvent(events.RL_GO_QUEUE); const { navigation, isMasterDetail, inquiryEnabled } = this.props; @@ -1008,7 +1029,8 @@ const mapStateToProps = (state: IApplicationState) => ({ createPublicChannelPermission: state.permissions['create-c'], createPrivateChannelPermission: state.permissions['create-p'], createDiscussionPermission: state.permissions['start-discussion'], - serverVersion: state.server.version + serverVersion: state.server.version, + issuesWithNotifications: state.troubleshootingNotification.issuesWithNotifications }); export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomsListView)))); diff --git a/app/views/UserNotificationPreferencesView/index.tsx b/app/views/UserNotificationPreferencesView/index.tsx index 739756440c..9d7a66600f 100644 --- a/app/views/UserNotificationPreferencesView/index.tsx +++ b/app/views/UserNotificationPreferencesView/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useState } from 'react'; import { Switch } from 'react-native'; import { StackNavigationProp } from '@react-navigation/stack'; -import { useNavigation } from '@react-navigation/native'; +import { CompositeNavigationProp, useNavigation } from '@react-navigation/native'; import StatusBar from '../../containers/StatusBar'; import * as List from '../../containers/List'; @@ -15,16 +15,25 @@ import { Services } from '../../lib/services'; import { useAppSelector } from '../../lib/hooks'; import ListPicker from './ListPicker'; import log from '../../lib/methods/helpers/log'; +import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { useUserPreferences } from '../../lib/methods'; import { NOTIFICATION_IN_APP_VIBRATION, SWITCH_TRACK_COLOR } from '../../lib/constants'; +type TNavigation = CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp +>; + const UserNotificationPreferencesView = () => { const [inAppVibration, setInAppVibration] = useUserPreferences(NOTIFICATION_IN_APP_VIBRATION, true); const [preferences, setPreferences] = useState({} as INotificationPreferences); const [loading, setLoading] = useState(true); - const navigation = useNavigation>(); - const userId = useAppSelector(state => getUserSelector(state).id); + const navigation = useNavigation(); + const { userId, isMasterDetail } = useAppSelector(state => ({ + userId: getUserSelector(state).id, + isMasterDetail: state.app.isMasterDetail + })); useLayoutEffect(() => { navigation.setOptions({ @@ -62,6 +71,14 @@ const UserNotificationPreferencesView = () => { } }; + const navigateToPushTroubleshootView = () => { + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { screen: 'PushTroubleshootView' }); + } else { + navigation.navigate('PushTroubleshootView'); + } + }; + const toggleInAppVibration = () => { setInAppVibration(!inAppVibration); }; @@ -97,6 +114,13 @@ const UserNotificationPreferencesView = () => { value={preferences.pushNotifications} /> + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c8554de0ec..1ba319c09f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -534,6 +534,11 @@ PODS: - React-Core - RNMathView (1.0.0): - iosMath + - RNNotifee (7.8.0): + - React-Core + - RNNotifee/NotifeeCore (= 7.8.0) + - RNNotifee/NotifeeCore (7.8.0): + - React-Core - RNReanimated (2.8.0): - DoubleConversion - FBLazyVector @@ -666,6 +671,7 @@ DEPENDENCIES: - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNLocalize (from `../node_modules/react-native-localize`) - RNMathView (from `../node_modules/react-native-math-view/ios`) + - "RNNotifee (from `../node_modules/@notifee/react-native`)" - RNReanimated (from `../node_modules/react-native-reanimated`) - RNRootView (from `../node_modules/rn-root-view`) - RNScreens (from `../node_modules/react-native-screens`) @@ -857,6 +863,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-localize" RNMathView: :path: "../node_modules/react-native-math-view/ios" + RNNotifee: + :path: "../node_modules/@notifee/react-native" RNReanimated: :path: "../node_modules/react-native-reanimated" RNRootView: @@ -972,6 +980,7 @@ SPEC CHECKSUMS: RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b RNMathView: 4c8a3c081fa671ab3136c51fa0bdca7ffb708bd5 + RNNotifee: f3c01b391dd8e98e67f539f9a35a9cbcd3bae744 RNReanimated: 64573e25e078ae6bec03b891586d50b9ec284393 RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494 RNScreens: fa9b582d85ae5d62c91c66003b5278458fed7aaa diff --git a/react-native.config.js b/react-native.config.js index 274dfa1619..1a0fd25743 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -19,11 +19,6 @@ module.exports = { platforms: { ios: null } - }, - '@notifee/react-native': { - platforms: { - ios: null - } } } }; From 2f4bde77495fad51980305b738871e2cdd4a00a7 Mon Sep 17 00:00:00 2001 From: Gleidson Daniel Silva Date: Wed, 6 Mar 2024 10:02:02 -0300 Subject: [PATCH 2/3] chore: update notification troubleshoot page copy (#5603) --- app/i18n/locales/en.json | 4 ++-- app/i18n/locales/pt-BR.json | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index f350b1a334..1cc406b30b 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -137,7 +137,7 @@ "Create_account": "Create an account", "Created_snippet": "created a snippet", "Custom_push_gateway_connected_description": "Your workspace uses a custom push notification gateway. Check with your workspace administrator for any issues.", - "Custom_push_gateway_connection": "Custom Gateway Connection", + "Custom_push_gateway_connection": "Custom gateway connection", "DELETE": "DELETE", "Dark": "Dark", "Dark_level": "Dark level", @@ -474,7 +474,7 @@ "Push_Notifications_Alert_Info": "These notifications are delivered to you when the app is not open", "Push_Troubleshooting": "Push Troubleshooting", "Push_gateway_connected_description": "Send a push notification to yourself to check if the gateway is working", - "Push_gateway_connection": "Push Gateway Connection", + "Push_gateway_connection": "Push gateway connection", "Push_gateway_not_connected_description": "We're not able to connect to the push gateway. If this issue persists please check with your workspace administrator.", "Queued_chats": "Queued chats", "Quote": "Quote", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 67e06b1d6b..b1cd1ab83b 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -23,7 +23,7 @@ "All_users_in_the_channel_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas", "All_users_in_the_team_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas", "Allow_Reactions": "Permitir reagir", - "Allow_push_notifications_for_rocket_chat": "Nenhuma ação adicional é necessária", + "Allow_push_notifications_for_rocket_chat": "Permitir notificações push para Rocket.Chat", "Also_send_thread_message_to_channel_behavior": "Também enviar mensagem do tópico para o canal", "Announcement": "Anúncio", "App_users_are_not_allowed_to_log_in_directly": "Usuários do aplicativo não estão autorizados a fazer login diretamente.", @@ -100,7 +100,7 @@ "Close_emoji_selector": "Fechar seletor de emojis", "Code_or_password_invalid": "Código ou senha inválido", "Collaborative": "Colaborativo", - "Community_edition_push_quota": "Cota de notificações push Community Edition", + "Community_edition_push_quota": "Cota de notificações push community edition", "Condensed": "Condensado", "Confirm": "Confirmar", "Confirmation": "Confirmação", @@ -134,7 +134,7 @@ "Create_account": "Criar conta", "Created_snippet": "criou um snippet", "Custom_push_gateway_connected_description": "Seu workspace utiliza um gateway de notificação push personalizado. Verifique com o administrador do seu workspace se há algum problema.", - "Custom_push_gateway_connection": "Conexão Personalizada com o Gateway", + "Custom_push_gateway_connection": "Conexão personalizada com o gateway", "DELETE": "EXCLUIR", "Dark": "Escuro", "Dark_level": "Nível escuro", @@ -390,7 +390,7 @@ "No_channels_in_team": "Nenhum canal nesta equipe", "No_discussions": "Sem discussões", "No_files": "Não há arquivos", - "No_further_action_is_needed": "Ir para configurações do dispositivo", + "No_further_action_is_needed": "Nenhuma ação adicional é necessária", "No_label_provided": "Sem {{label}}.", "No_limit": "Sem limite", "No_match_found": "Nenhum resultado encontrado.", @@ -466,7 +466,7 @@ "Push_Notifications_Alert_Info": "Essas notificações são entregues a você quando o aplicativo não está aberto", "Push_Troubleshooting": "Solucionar Problemas de Push", "Push_gateway_connected_description": "Envie uma notificação push para si mesmo para verificar se o gateway está funcionando.", - "Push_gateway_connection": "Conexão com o Gateway de Push", + "Push_gateway_connection": "Conexão com o gateway de push", "Push_gateway_not_connected_description": "Não conseguimos conectar ao gateway de push. Se esse problema persistir, por favor, verifique com o administrador do seu workspace.", "Queued_chats": "Bate-papos na fila", "Quote": "Citar", @@ -695,7 +695,7 @@ "Wi_Fi_and_mobile_data": "Wi-Fi e dados móveis", "Without_Servers": "Sem workspaces", "Workspace_URL_Example": "Ex. sua-empresa.rocket.chat", - "Workspace_consumption": "Consumo do Workspace", + "Workspace_consumption": "Consumo do workspace", "Workspace_consumption_description": "Existe uma quantidade definida de notificações push por mês", "Workspaces": "Workspaces", "Would_like_to_place_on_hold": "Gostaria de colocar essa conversa em espera?", From 2c0cfa168187ccaefd73811251c8e38c41c07e69 Mon Sep 17 00:00:00 2001 From: Gleidson Daniel Silva Date: Wed, 6 Mar 2024 10:04:12 -0300 Subject: [PATCH 3/3] regression: validate that the login error value is actually a string (#5602) --- app/views/LoginView/handleLoginErrors.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/LoginView/handleLoginErrors.ts b/app/views/LoginView/handleLoginErrors.ts index 37f1b2080c..5e352b2864 100644 --- a/app/views/LoginView/handleLoginErrors.ts +++ b/app/views/LoginView/handleLoginErrors.ts @@ -26,10 +26,12 @@ const LOGIN_SUBMIT_ERRORS = { }; export const handleLoginErrors = (error: keyof typeof LOGIN_SUBMIT_ERRORS): string => { - const errorKey = Object.keys(LOGIN_SUBMIT_ERRORS).find(key => error.includes(key)) as keyof typeof LOGIN_SUBMIT_ERRORS; - const e = errorKey ? LOGIN_SUBMIT_ERRORS[errorKey].i18n : 'Login_error'; - if (i18n.isTranslated(e)) { - return i18n.t(e); + if (typeof error === 'string') { + const errorKey = Object.keys(LOGIN_SUBMIT_ERRORS).find(key => error?.includes(key)) as keyof typeof LOGIN_SUBMIT_ERRORS; + const e = errorKey ? LOGIN_SUBMIT_ERRORS[errorKey]?.i18n : 'Login_error'; + if (i18n.isTranslated(e)) { + return i18n.t(e); + } } return i18n.t('Login_error'); };