mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-02 05:06:51 +08:00
**Previously**, the "Reply" action on chat web push notifications was
silently broken — the service worker's chat URL regex stopped matching
when chat routes moved to `/chat/c/:slug/:id`, and the chat-specific
code lived in core with no thread support, URL encoding, or error
handling.
**In this update**, generalize the core service worker into a
plugin-extensible action registry, move chat reply handling into the
chat plugin with proper thread/encoding/fallback support, and convert
single-emoji replies into reactions on the source message (falling back
to a regular message post on any failure).
## Architecture
Core (`app/views/static/service-worker.js.erb`) now exposes:
```js
self.registerNotificationActionHandler(action, handler);
```
Push payloads can carry `actions` (Web Notifications API format) and
`action_data` (an arbitrary object surfaced to the handler at
`event.notification.data.actionData`). The click handler dispatches by
`event.action` name and falls back to the existing focus-or-open
behavior when no handler matches or a handler throws.
`PushNotificationPusher` forwards `actions` / `action_data` when
present.
The chat plugin owns its own service worker
(`plugins/chat/assets/javascripts/service-worker.js`), registered via
`register_service_worker`. It registers a `chat-reply` handler that:
1. Fetches a CSRF token.
2. If the reply is a single emoji grapheme (detected via
`Intl.Segmenter`, with a `\p{Extended_Pictographic}` regex fallback),
`PUT`s to `/chat/:channel_id/react/:message_id`.
3. Otherwise — or if the reaction request fails — `POST`s to
`/chat/:channel_id` with proper `URLSearchParams` encoding and the
source `thread_id` if present.
4. If everything fails, opens the channel so the user can retype.
`Chat::Notifier.push_notification_reply_action(chat_message, user)`
builds the payload in the user's locale with `channel_id`, `message_id`,
and optional `thread_id`.
`Chat::MessageReactor#react!` resolves raw Unicode via
`Emoji.unicode_replacements[emoji] || emoji` before validation, so
smartwatch quick-reply chips (which send raw Unicode like `👍🏽` or ZWJ
sequences) just work. Existing shortcode callers are unaffected.
## Why this matters
Smartwatch notification UIs expose a single "reply" affordance with
canned chips that include short text and emoji. The user can't choose
between "send as message" and "react"; they tap a chip and a string
comes through. Treating a single-grapheme pictographic reply as a
reaction matches user intent on the watch UX without any new product
surface.
## Test plan
- [ ] On a chat-enabled site, subscribe to web push notifications.
- [ ] Have another user post a message that mentions you, or a watched
channel message.
- [ ] On the resulting OS notification, confirm a "Reply" action button
appears.
- [ ] Type a text reply and submit — verify it posts as a message in the
right channel/thread.
- [ ] Submit a single emoji (e.g. 👍 or 👍🏽 with skin tone) — verify it
lands as a reaction on the source message instead of posting a message.
- [ ] Submit a ZWJ sequence (e.g. 👨👩👧) — verify it lands as a reaction
(or falls back to message post if not in the emoji DB).
- [ ] Sign out / let session expire and submit a reply — verify the
channel opens as a fallback rather than silently failing.
- [ ] Watch via Wear OS / watchOS quick-reply chips and confirm the same
behavior end-to-end.
328 lines
20 KiB
YAML
328 lines
20 KiB
YAML
en:
|
||
site_settings:
|
||
chat_separate_sidebar_mode: "Show separate sidebar modes for forum and chat."
|
||
chat_enabled: "Enable chat."
|
||
chat_pinned_messages: "Enable pinned messages in chat channels."
|
||
enable_public_channels: "Enable public channels based on categories."
|
||
chat_allowed_groups: "Users in these groups can chat. Note that admins and moderators can always access chat."
|
||
chat_channel_retention_days: "Chat messages in regular channels will be retained for this many days. Set to '0' to retain messages forever."
|
||
chat_dm_retention_days: "Chat messages in personal chat channels will be retained for this many days. Set to '0' to retain messages forever."
|
||
chat_auto_silence_duration: "Number of minutes that users will be silenced for when they exceed the chat message creation rate limit. Set to '0' to disable auto-silencing."
|
||
chat_allowed_messages_for_trust_level_0: "Number of messages that trust level 0 users is allowed to send in 30 seconds. Set to '0' to disable limit."
|
||
chat_allowed_messages_for_other_trust_levels: "Number of messages that users with trust levels 1-4 is allowed to send in 30 seconds. Set to '0' to disable limit."
|
||
chat_silence_user_sensitivity: "The likelihood that a user flagged in chat will be automatically silenced."
|
||
chat_auto_silence_from_flags_duration: "Number of minutes that users will be silenced for when they are automatically silenced due to flagged chat messages."
|
||
chat_duplicate_message_sensitivity: "The likelihood that a duplicate message by the same sender will be blocked in a short period. Decimal number between 0 and 1.0, with 1.0 being the highest setting (blocks messages more frequently in a shorter amount of time). Set to `0` to allow duplicate messages."
|
||
chat_minimum_message_length: "Minimum number of characters for a chat message."
|
||
chat_allow_uploads: "Allow uploads in public chat channels and direct message channels."
|
||
chat_show_copy_button_on_codeblocks: "Show copy button on codeblocks in chat messages."
|
||
chat_archive_destination_topic_status: "The status that the destination topic should be once a channel archive is completed. This only applies when the destination topic is a new topic, not an existing one."
|
||
direct_message_enabled_groups: "Allow users within these groups to create user-to-user Personal Chats. Note: staff can always create Personal Chats, and users will be able to reply to Personal Chats initiated by users who have permission to create them."
|
||
chat_message_flag_allowed_groups: "Users in these groups are allowed to flag chat messages. Note that admins and moderators can always flag chat messages."
|
||
chat_pinning_messages_allowed_groups: "Users in these groups are allowed to pin chat messages in channels that they can access. Note that admins and moderators can always pin chat messages. Any member of a group chat or DM can pin messages there."
|
||
max_mentions_per_chat_message: "Maximum number of @name notifications a user can use in a chat message."
|
||
chat_max_direct_message_users: "Users cannot add more than this number of other users when creating a new direct message. Set to 0 to only allow messages to oneself. Staff are exempt from this setting."
|
||
chat_allow_archiving_channels: "Allow staff to archive messages to a topic when closing a channel."
|
||
chat_editing_grace_period: "For (n) seconds after sending a chat, editing will not display the (edited) tag by the chat message."
|
||
chat_editing_grace_period_max_diff_low_trust: "Maximum number of character changes allowed in chat editing grace period, if more changed display the (edited) tag by the chat message (trust level 0 and 1)."
|
||
chat_editing_grace_period_max_diff_high_trust: "Maximum number of character changes allowed in chat editing grace period, if more changed display the (edited) tag by the chat message (trust level 2 and up)."
|
||
chat_preferred_index: "Preferred tab when loading /chat."
|
||
allow_chat_in_anonymous_mode: "Enable this setting to allow users who are browsing your site anonymously to use chat. This setting requires the `allow anonymous mode` setting to be enabled."
|
||
chat_search_enabled: "Enable this setting to allow users to search chat messages."
|
||
errors:
|
||
chat_default_channel: "The default chat channel must be a public channel."
|
||
direct_message_enabled_groups_invalid: "You must specify at least one group for this setting. If you do not want anyone except staff to send direct messages, choose the staff group."
|
||
chat_upload_not_allowed_secure_uploads: "Chat uploads are not allowed when secure uploads site setting is enabled."
|
||
allow_chat_in_anonymous_mode_invalid: "This setting requires the 'allow anonymous mode' setting to be enabled first."
|
||
system_messages:
|
||
private_channel_title: "Channel %{id}"
|
||
chat_channel_archive_complete:
|
||
title: "Chat Channel Archive Complete"
|
||
subject_template: "Chat channel archive completed successfully"
|
||
text_body_template: |
|
||
Archiving the chat channel %{channel_hashtag_or_name} has been completed successfully. The messages were copied into the topic [%{topic_title}](%{topic_url}).
|
||
chat_channel_archive_failed:
|
||
title: "Chat Channel Archive Failed"
|
||
subject_template: "Chat channel archive failed"
|
||
text_body_template: |
|
||
Archiving the chat channel %{channel_hashtag_or_name} has failed. %{messages_archived} messages have been archived. Partially archived messages were copied into the topic [%{topic_title}](%{topic_url}). Visit the channel at %{channel_url} to retry.
|
||
chat_channel_archive_failed_no_topic:
|
||
title: "Chat Channel Archive Failed"
|
||
subject_template: "Chat channel archive failed"
|
||
text_body_template: |
|
||
Archiving the chat channel %{channel_hashtag_or_name} has failed. No messages have been archived. The topic was not created successfully for the following reasons:
|
||
|
||
%{topic_validation_errors}
|
||
|
||
Visit the channel at %{channel_url} to retry.
|
||
|
||
chat:
|
||
deleted_chat_username: deleted
|
||
errors:
|
||
users_cant_be_added_to_channel: "Users can't be added to this channel."
|
||
user_cant_be_removed_from_channel: "User can't be removed from this channel"
|
||
channel_exists_for_category: "A channel already exists for this category and name"
|
||
channel_new_message_disallowed:
|
||
archived: "The channel is archived, no new messages can be sent"
|
||
closed: "The channel is closed, no new messages can be sent"
|
||
read_only: "The channel is read only, no new messages can be sent"
|
||
channel_modify_message_disallowed:
|
||
archived: "The channel is archived, no messages can be edited or deleted"
|
||
closed: "The channel is closed, no messages can be edited or deleted"
|
||
read_only: "The channel is read only, no messages can be edited or deleted"
|
||
user_cannot_send_message: "You cannot send messages at this time."
|
||
user_not_in_channel: "You must be a member of this channel."
|
||
rate_limit_exceeded: "Exceeded the limit of chat messages that can be sent within 30 seconds"
|
||
auto_silence_from_flags: "Chat message flagged with score high enough to silence user."
|
||
channel_cannot_be_archived: "The channel cannot be archived at this time, it must be either closed or open to archive."
|
||
duplicate_message: "You posted an identical message too recently."
|
||
delete_channel_failed: "Delete channel failed, please try again."
|
||
minimum_length_not_met:
|
||
one: "Message is too short, must have a minimum of %{count} character."
|
||
other: "Message is too short, must have a minimum of %{count} characters."
|
||
message_too_long:
|
||
one: "Message is too long, messages must be a maximum of %{count} character."
|
||
other: "Message is too long, messages must be a maximum of %{count} characters."
|
||
draft_too_long: "Draft is too long."
|
||
max_reactions_limit_reached: "New reactions are not allowed on this message."
|
||
message_move_invalid_channel: "The source and destination channel must be public channels."
|
||
message_move_no_messages_found: "No messages were found with the provided message IDs."
|
||
cant_update_direct_message_channel: "Direct message channel properties like name and description can’t be updated."
|
||
not_accepting_dms: "Sorry, %{username} is not accepting messages at the moment."
|
||
actor_ignoring_target_user: "You are ignoring %{username}, so you cannot send messages to them."
|
||
actor_muting_target_user: "You are muting %{username}, so you cannot send messages to them."
|
||
actor_disallowed_dms: "You have chosen to prevent users from sending you personal and direct messages, so you cannot create new direct messages."
|
||
actor_preventing_target_user_from_dm: "You have chosen to prevent %{username} from sending you personal and direct messages, so you cannot create new direct messages to them."
|
||
not_reachable: "Sorry, %{username} is not using chat."
|
||
user_cannot_send_direct_messages: "Sorry, you cannot send direct messages."
|
||
over_chat_max_direct_message_users_allow_self: "You can only create a direct message with yourself."
|
||
over_chat_max_direct_message_users:
|
||
one: "You can't create a direct message with more than %{count} other user."
|
||
other: "You can't create a direct message with more than %{count} other users."
|
||
original_message_not_found: "The ancestor of the message you are replying to cannot be found or has been deleted."
|
||
thread_invalid_for_channel: "Thread is not part of the provided channel."
|
||
thread_does_not_match_parent: "Thread does not match parent message."
|
||
invalid_direct_message: "You are not allowed to create this direct message."
|
||
pin_limit_reached: "This channel has reached the maximum number of pinned messages (%{limit})."
|
||
message_already_pinned: "This message is already pinned."
|
||
message_not_pinned: "This message is not pinned."
|
||
reviewables:
|
||
message_already_handled: "Thanks, but we've already reviewed this message and determined it does not need to be flagged again."
|
||
actions:
|
||
message_actions:
|
||
bundle_title: "What do you want to do with the message?"
|
||
no_action_message:
|
||
title: "No action"
|
||
description: "Leave the message as is."
|
||
delete_message:
|
||
title: "Delete message"
|
||
description: "Permanently delete the message."
|
||
restore_message:
|
||
title: "Restore message"
|
||
description: "Restore the message so that users can see it."
|
||
agree:
|
||
title: "Agree..."
|
||
agree_and_keep_message:
|
||
title: "Keep Message"
|
||
description: "Agree with flag and keep the message unchanged."
|
||
agree_and_keep_deleted:
|
||
title: "Keep Message Deleted"
|
||
description: "Agree with flag and leave the message deleted."
|
||
agree_and_suspend:
|
||
title: "Suspend User"
|
||
description: "Agree with flag and suspend the user."
|
||
agree_and_silence:
|
||
title: "Silence User"
|
||
description: "Agree with flag and silence the user."
|
||
agree_and_restore:
|
||
title: "Restore Message"
|
||
description: "Restore the message so that users can see it."
|
||
agree_and_delete:
|
||
title: "Delete Message"
|
||
description: "Delete the message so that users cannot see it."
|
||
delete_and_agree:
|
||
title: "Ignore flag and delete message"
|
||
description: "Ignore the flag by removing it from the queue and delete the message."
|
||
disagree_and_restore:
|
||
title: "Disagree and Restore Message"
|
||
description: "Restore the message so that all users can see it."
|
||
disagree:
|
||
title: "Keep message"
|
||
description: "Disagree with flag and leave message unchanged."
|
||
ignore:
|
||
title: "Ignore flag"
|
||
description: "Remove it from the queue without taking action."
|
||
direct_messages:
|
||
transcript_title: "Transcript of previous messages in %{channel_name}"
|
||
transcript_body: "To give you more context, we included a transcript of the previous messages in this conversation (up to ten):\n\n%{transcript}"
|
||
channel:
|
||
users_invited_to_channel:
|
||
one: "%{invited_users} has been invited by %{inviting_user}."
|
||
other: "%{invited_users} have been invited by %{inviting_user}."
|
||
archive:
|
||
first_post_raw: "This topic is an archive of the [%{channel_name}](%{channel_url}) chat channel."
|
||
messages_moved:
|
||
one: "@%{acting_username} moved a message to the [%{channel_name}](%{first_moved_message_url}) channel."
|
||
other: "@%{acting_username} moved %{count} messages to the [%{channel_name}](%{first_moved_message_url}) channel."
|
||
dm_title:
|
||
single_user: "%{username}"
|
||
multi_user: "%{comma_separated_usernames}"
|
||
multi_user_truncated:
|
||
one: "%{comma_separated_usernames} and %{count} other"
|
||
other: "%{comma_separated_usernames} and %{count} others"
|
||
mention_warning:
|
||
dismiss: "dismiss"
|
||
cannot_see: "%{first_identifier} can't access this channel and was not notified."
|
||
cannot_see_group: "Some members of %{group_name} can't access this channel and were not notified."
|
||
cannot_see_multiple:
|
||
one: "%{first_identifier} and %{count} other user cannot access this channel and were not notified."
|
||
other: "%{first_identifier} and %{count} other users cannot access this channel and were not notified."
|
||
invitations_sent:
|
||
one: "Invitation sent"
|
||
other: "Invitations sent"
|
||
invite: "Invite to channel"
|
||
without_membership: "%{first_identifier} has not joined this channel."
|
||
without_membership_multiple:
|
||
one: "%{first_identifier} and %{count} other user have not joined this channel."
|
||
other: "%{first_identifier} and %{count} other users have not joined this channel."
|
||
group_mentions_disabled: "%{first_identifier} doesn't allow mentions."
|
||
group_mentions_disabled_multiple:
|
||
one: "%{first_identifier} and %{count} other group don't allow mentions."
|
||
other: "%{first_identifier} and %{count} other groups don't allow mentions."
|
||
global_mentions_disallowed: "@here and @all mentions are disabled in this channel."
|
||
too_many_members: "%{first_identifier} has too many members. No one was notified."
|
||
too_many_members_multiple:
|
||
one: "%{first_identifier} and %{count} other group have too many members. No one was notified."
|
||
other: "%{first_identifier} and %{count} other groups have too many members. No one was notified."
|
||
|
||
category_channel:
|
||
errors:
|
||
slug_contains_non_ascii_chars: "contains non-ascii characters"
|
||
is_already_in_use: "is already in use"
|
||
|
||
bookmarkable:
|
||
notification_title_channel: "message in %{channel_name}"
|
||
notification_title_direct_message: "message with %{channel_name}"
|
||
|
||
personal_chat: "personal chat"
|
||
|
||
onebox:
|
||
inline_to_message: "Message #%{message_id} by %{username} – #%{chat_channel}"
|
||
inline_to_channel: "Chat #%{chat_channel}"
|
||
inline_to_topic_channel: "Chat for Topic %{topic_title}"
|
||
inline_to_thread: "%{thread_title} - #%{chat_channel}"
|
||
inline_to_thread_no_title: "Thread in #%{chat_channel}"
|
||
thread_title_connector: "in"
|
||
|
||
x_members:
|
||
one: "%{count} member"
|
||
other: "%{count} members"
|
||
|
||
and_x_others:
|
||
one: "and %{count} other"
|
||
other: "and %{count} others"
|
||
|
||
transcript:
|
||
default_thread_title: "Thread"
|
||
split_thread_range: "messages %{start} to %{end} of %{total}"
|
||
|
||
discourse_push_notifications:
|
||
popup:
|
||
chat_mention:
|
||
direct: '%{username} mentioned you in "%{channel}"'
|
||
other_type: '%{username} mentioned %{identifier} in "%{channel}"'
|
||
direct_message_chat_mention:
|
||
direct: "%{username} mentioned you in personal chat"
|
||
other_type: "%{username} mentioned %{identifier} in personal chat"
|
||
new_chat_message: '%{username} sent a message in "%{channel}"'
|
||
new_direct_chat_message: "%{username} sent a message in personal chat"
|
||
actions:
|
||
chat_reply:
|
||
title: Reply
|
||
placeholder: Reply…
|
||
|
||
discourse_automation:
|
||
scriptables:
|
||
send_chat_message:
|
||
title: Send chat message
|
||
|
||
reviewable_score_types:
|
||
needs_review:
|
||
title: "Needs Review"
|
||
notify_user:
|
||
chat_pm_title: 'Your chat message in "%{channel_name}"'
|
||
chat_pm_body: "%{link}\n\n%{message}"
|
||
notify_moderators:
|
||
chat_pm_title: 'A chat message in "%{channel_name}" requires staff attention'
|
||
chat_pm_body: "%{link}\n\n%{message}"
|
||
|
||
reviewables:
|
||
reasons:
|
||
chat_message_queued_by_staff: "A staff member thinks this chat message needs review."
|
||
user_notifications:
|
||
chat_summary:
|
||
deleted_user: "Deleted user"
|
||
description:
|
||
one: "You have a new chat message"
|
||
other: "You have new chat messages"
|
||
from: "%{site_name}"
|
||
subject:
|
||
private_email:
|
||
one: "[%{site_name}] New message"
|
||
other: "[%{site_name}] New messages"
|
||
private_email_improved:
|
||
one: "New message"
|
||
other: "New messages"
|
||
watched_thread:
|
||
one: "[%{site_name}] New thread message in %{channel}"
|
||
other: "[%{site_name}] New thread messages in %{channel}"
|
||
watched_thread_improved:
|
||
one: "New thread message in %{channel}"
|
||
other: "New thread messages in %{channel}"
|
||
watched_threads:
|
||
one: "[%{site_name}] New thread messages in %{channel} and %{count} other channel"
|
||
other: "[%{site_name}] New thread messages in %{channel} and %{count} other channels"
|
||
watched_threads_improved:
|
||
one: "New thread messages in %{channel} and %{count} other channel"
|
||
other: "New thread messages in %{channel} and %{count} other channels"
|
||
chat_dm_1:
|
||
one: "[%{site_name}] New message from %{name}"
|
||
other: "[%{site_name}] New messages from %{name}"
|
||
chat_dm_1_improved:
|
||
one: "New message from %{name}"
|
||
other: "New messages from %{name}"
|
||
chat_dm_2: "[%{site_name}] New messages from %{name_1} and %{name_2}"
|
||
chat_dm_2_improved: "New messages from %{name_1} and %{name_2}"
|
||
chat_dm_3_or_more: "[%{site_name}] New messages from %{name} and %{count} other direct message channels"
|
||
chat_dm_3_or_more_improved: "New messages from %{name} and %{count} other direct message channels"
|
||
chat_channel_1:
|
||
one: "[%{site_name}] New message in %{channel}"
|
||
other: "[%{site_name}] New messages in %{channel}"
|
||
chat_channel_1_improved:
|
||
one: "New message in %{channel}"
|
||
other: "New messages in %{channel}"
|
||
chat_channel_2: "[%{site_name}] New messages in %{channel_1} and %{channel_2}"
|
||
chat_channel_2_improved: "New messages in %{channel_1} and %{channel_2}"
|
||
chat_channel_3_or_more: "[%{site_name}] New messages in %{channel} and %{count} other channels"
|
||
chat_channel_3_or_more_improved: "New messages in %{channel} and %{count} other channels"
|
||
chat_channel_and_dm: "[%{site_name}] New messages in %{channel} and from %{name}"
|
||
chat_channel_and_dm_improved: "New messages in %{channel} and from %{name}"
|
||
unsubscribe: "This chat summary is sent from %{site_link} when you are away. Change your %{email_preferences_link}, or %{unsubscribe_link} to unsubscribe."
|
||
unsubscribe_no_link: "This chat summary is sent from %{site_link} when you are away. Change your %{email_preferences_link}."
|
||
view_messages:
|
||
one: "View message"
|
||
other: "View %{count} messages"
|
||
view_more:
|
||
one: "View %{count} more message"
|
||
other: "View %{count} more messages"
|
||
your_chat_settings: "chat email preference"
|
||
|
||
unsubscribe:
|
||
chat_summary:
|
||
select_title: "Set chat summary emails to:"
|
||
never: Never
|
||
when_away: Only when away
|
||
|
||
category:
|
||
cannot_delete:
|
||
has_chat_channels: "Can't delete this category because it has chat channels."
|