From cd28e8854195be32db4d52694ddbb17d16e43737 Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Fri, 23 Apr 2021 17:10:08 +0800 Subject: [PATCH 1/8] webview [nfc]: Pass GetText into messageAsHtml. This will allow us to translate strings in the UI around messages. --- src/webview/MessageList.js | 2 ++ src/webview/generateInboundEvents.js | 1 + src/webview/html/contentHtmlFromPieceDescriptors.js | 6 ++++-- src/webview/html/messageAsHtml.js | 8 +++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/webview/MessageList.js b/src/webview/MessageList.js index 8712cf605f7..a33bb19019e 100644 --- a/src/webview/MessageList.js +++ b/src/webview/MessageList.js @@ -236,11 +236,13 @@ class MessageList extends Component { initialScrollMessageId, narrow, showMessagePlaceholders, + _, } = this.props; const contentHtml = contentHtmlFromPieceDescriptors({ backgroundData, narrow, htmlPieceDescriptors: htmlPieceDescriptorsForShownMessages, + _, }); const { auth, theme } = backgroundData; const html: string = getHtml(contentHtml, theme, { diff --git a/src/webview/generateInboundEvents.js b/src/webview/generateInboundEvents.js index c0828ac5a2a..407a87f1e56 100644 --- a/src/webview/generateInboundEvents.js +++ b/src/webview/generateInboundEvents.js @@ -51,6 +51,7 @@ const updateContent = (prevProps: Props, nextProps: Props): WebViewInboundEventC backgroundData: nextProps.backgroundData, narrow: nextProps.narrow, htmlPieceDescriptors: nextProps.htmlPieceDescriptorsForShownMessages, + _: nextProps._, }), nextProps.showMessagePlaceholders, ); diff --git a/src/webview/html/contentHtmlFromPieceDescriptors.js b/src/webview/html/contentHtmlFromPieceDescriptors.js index 7a2ef86c5f1..d31a05ce026 100644 --- a/src/webview/html/contentHtmlFromPieceDescriptors.js +++ b/src/webview/html/contentHtmlFromPieceDescriptors.js @@ -1,5 +1,5 @@ /* @flow strict-local */ -import type { Narrow, HtmlPieceDescriptor } from '../../types'; +import type { GetText, Narrow, HtmlPieceDescriptor } from '../../types'; import type { BackgroundData } from '../MessageList'; import messageAsHtml from './messageAsHtml'; @@ -10,10 +10,12 @@ export default ({ backgroundData, narrow, htmlPieceDescriptors, + _, }: {| backgroundData: BackgroundData, narrow: Narrow, htmlPieceDescriptors: HtmlPieceDescriptor[], + _: GetText, |}): string => { const pieces = []; htmlPieceDescriptors.forEach(section => { @@ -24,7 +26,7 @@ export default ({ if (item.type === 'time') { pieces.push(timeRowAsHtml(item.timestamp, item.subsequentMessage)); } else { - pieces.push(messageAsHtml(backgroundData, item.message, item.isBrief)); + pieces.push(messageAsHtml(backgroundData, item.message, item.isBrief, _)); } }); }); diff --git a/src/webview/html/messageAsHtml.js b/src/webview/html/messageAsHtml.js index 3e94e5f78c2..4eebede8548 100644 --- a/src/webview/html/messageAsHtml.js +++ b/src/webview/html/messageAsHtml.js @@ -5,6 +5,7 @@ import template from './template'; import type { AggregatedReaction, FlagsState, + GetText, Message, MessageLike, Outbox, @@ -82,7 +83,12 @@ $!${message.content} export const flagsStateToStringList = (flags: FlagsState, id: number): string[] => Object.keys(flags).filter(key => flags[key][id]); -export default (backgroundData: BackgroundData, message: Message | Outbox, isBrief: boolean) => { +export default ( + backgroundData: BackgroundData, + message: Message | Outbox, + isBrief: boolean, + _: GetText, +) => { const { id, timestamp } = message; const flagStrings = flagsStateToStringList(backgroundData.flags, id); const divOpenHtml = template` From 5f816db8c08642350de5899e1ef244d7d12e14cb Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Tue, 20 Apr 2021 02:25:34 +0800 Subject: [PATCH 2/8] webview: Hide messages from muted users, with option to show them. --- src/webview/html/messageAsHtml.js | 11 +++++++++ src/webview/js/generatedEs3.js | 23 ++++++++++++++++- src/webview/js/js.js | 37 +++++++++++++++++++++++++++- src/webview/static/base.css | 16 +++++++++++- static/translations/messages_en.json | 1 + 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/webview/html/messageAsHtml.js b/src/webview/html/messageAsHtml.js index 4eebede8548..a39ca7bc4ea 100644 --- a/src/webview/html/messageAsHtml.js +++ b/src/webview/html/messageAsHtml.js @@ -91,11 +91,14 @@ export default ( ) => { const { id, timestamp } = message; const flagStrings = flagsStateToStringList(backgroundData.flags, id); + const isUserMuted = !!message.sender_id && backgroundData.mutedUsers.has(message.sender_id); + const divOpenHtml = template`
template`data-${flag}="true" `).join('')} >`; const messageTime = shortTime(new Date(timestamp * 1000), backgroundData.twentyFourHourTime); @@ -140,6 +143,13 @@ $!${divOpenHtml}
${messageTime}
`; + const mutedMessageHtml = isUserMuted + ? template` +
+ ${_('This message was hidden because it is from a user you have muted. Long-press to view.')} +
+` + : ''; return template` $!${divOpenHtml} @@ -150,6 +160,7 @@ $!${divOpenHtml} $!${subheaderHtml} $!${bodyHtml} + $!${mutedMessageHtml} `; }; diff --git a/src/webview/js/generatedEs3.js b/src/webview/js/generatedEs3.js index 5e56c6f3c60..cbde6226441 100644 --- a/src/webview/js/generatedEs3.js +++ b/src/webview/js/generatedEs3.js @@ -519,6 +519,10 @@ var compiledWebviewJs = (function (exports) { return walkToMessage(start.previousElementSibling, 'previousElementSibling'); } + function nextMessage(start) { + return walkToMessage(start.nextElementSibling, 'nextElementSibling'); + } + function isVisible(element, top, bottom) { var rect = element.getBoundingClientRect(); return top < rect.bottom && rect.top < bottom; @@ -861,6 +865,15 @@ var compiledWebviewJs = (function (exports) { scrollEventsDisabled = false; }; + var revealMutedMessages = function revealMutedMessages(message) { + var messageNode = message; + + do { + messageNode.setAttribute('data-mute-state', 'shown'); + messageNode = nextMessage(messageNode); + } while (messageNode && messageNode.classList.contains('message-brief')); + }; + var requireAttribute = function requireAttribute(e, name) { var value = e.getAttribute(name); @@ -1003,9 +1016,17 @@ var compiledWebviewJs = (function (exports) { return; } + var targetType = target.matches('.header') ? 'header' : target.matches('a') ? 'link' : 'message'; + var messageNode = target.closest('.message'); + + if (targetType === 'message' && messageNode && messageNode.getAttribute('data-mute-state') === 'hidden') { + revealMutedMessages(messageNode); + return; + } + sendMessage({ type: 'longPress', - target: target.matches('.header') ? 'header' : target.matches('a') ? 'link' : 'message', + target: targetType, messageId: getMessageIdFromNode(target), href: target.matches('a') ? requireAttribute(target, 'href') : null }); diff --git a/src/webview/js/js.js b/src/webview/js/js.js index 3ae92c6b80e..607a1cb4a90 100644 --- a/src/webview/js/js.js +++ b/src/webview/js/js.js @@ -312,6 +312,11 @@ function previousMessage(start: Element): ?Element { return walkToMessage(start.previousElementSibling, 'previousElementSibling'); } +/** The message after the given message, if any. */ +function nextMessage(start: Element): ?Element { + return walkToMessage(start.nextElementSibling, 'nextElementSibling'); +} + /** * An element is visible if any part of it is visible on screen. * @@ -772,6 +777,18 @@ const handleMessageEvent: MessageEventListener = e => { * */ +/** + * If the given message is muted, reveal it and all consecutive following + * messages from the same user. + */ +const revealMutedMessages = (message: Element) => { + let messageNode = message; + do { + messageNode.setAttribute('data-mute-state', 'shown'); + messageNode = nextMessage(messageNode); + } while (messageNode && messageNode.classList.contains('message-brief')); +}; + const requireAttribute = (e: Element, name: string): string => { const value = e.getAttribute(name); if (value === null || value === undefined) { @@ -926,9 +943,27 @@ const handleLongPress = (target: Element) => { return; } + // Prettier bug on nested ternary + /* prettier-ignore */ + const targetType = target.matches('.header') + ? 'header' + : target.matches('a') + ? 'link' + : 'message'; + const messageNode = target.closest('.message'); + + if ( + targetType === 'message' + && messageNode + && messageNode.getAttribute('data-mute-state') === 'hidden' + ) { + revealMutedMessages(messageNode); + return; + } + sendMessage({ type: 'longPress', - target: target.matches('.header') ? 'header' : target.matches('a') ? 'link' : 'message', + target: targetType, messageId: getMessageIdFromNode(target), href: target.matches('a') ? requireAttribute(target, 'href') : null, }); diff --git a/src/webview/static/base.css b/src/webview/static/base.css index 74e36cb4f5a..2c8f04e1a2b 100644 --- a/src/webview/static/base.css +++ b/src/webview/static/base.css @@ -142,6 +142,19 @@ body { padding: 0 16px 16px 80px; } +.message-brief[data-mute-state="hidden"] { + display: none; +} + +.message[data-mute-state="hidden"] > .avatar, +.message[data-mute-state="hidden"] > .content { + display: none; +} + +.message[data-mute-state="shown"] > .muted-message-explanation { + display: none; +} + #message-loading { position: fixed; width: 100%; @@ -580,7 +593,8 @@ h1, h2, h3, h4, h5, h6 { text-decoration: underline; } -/* Our own "sorry, unsupported" message for widgets. */ +/* Our own "sorry, unsupported" message for widgets. + Also used for showing that a message was sent by a muted user. */ .special-message { display: flex; flex-direction: column; diff --git a/static/translations/messages_en.json b/static/translations/messages_en.json index c2472a61ec5..334bf1d9319 100644 --- a/static/translations/messages_en.json +++ b/static/translations/messages_en.json @@ -228,6 +228,7 @@ "🤒 Out sick": "🤒 Out sick", "🌴 Vacationing": "🌴 Vacationing", "🏠 Working remotely": "🏠 Working remotely", + "This message was hidden because it is from a user you have muted. Long-press to view.": "This message was hidden because it is from a user you have muted. Long-press to view.", "Signed out": "Signed out", "Remove account?": "Remove account?", "This will make the mobile app on this device forget {email} on {realmUrl}.": "This will make the mobile app on this device forget {email} on {realmUrl}.", From a56e0b4badef47a8865386a4e366a2b97aea1385 Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Tue, 20 Apr 2021 17:51:18 +0800 Subject: [PATCH 3/8] PmConversationList [nfc]: Convert to functional component. --- src/pm-conversations/PmConversationList.js | 82 ++++++++++++---------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/src/pm-conversations/PmConversationList.js b/src/pm-conversations/PmConversationList.js index 093f185f422..849637769be 100644 --- a/src/pm-conversations/PmConversationList.js +++ b/src/pm-conversations/PmConversationList.js @@ -1,5 +1,5 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React, { useCallback } from 'react'; import { FlatList } from 'react-native'; import type { Dispatch, PmConversationData, UserOrBot } from '../types'; @@ -25,45 +25,49 @@ type Props = $ReadOnly<{| /** * A list describing all PM conversations. * */ -export default class PmConversationList extends PureComponent { - handleUserNarrow = (user: UserOrBot) => { - this.props.dispatch(doNarrow(pm1to1NarrowFromUser(user))); - }; +export default function PmConversationList(props: Props) { + const handleUserNarrow = useCallback( + (user: UserOrBot) => { + props.dispatch(doNarrow(pm1to1NarrowFromUser(user))); + }, + [props.dispatch], + ); - handleGroupNarrow = (users: PmKeyUsers) => { - this.props.dispatch(doNarrow(pmNarrowFromUsers(users))); - }; + const handleGroupNarrow = useCallback( + (users: PmKeyUsers) => { + props.dispatch(doNarrow(pmNarrowFromUsers(users))); + }, + [props.dispatch], + ); - render() { - const { conversations } = this.props; + const { conversations } = props; - return ( - item.key} - renderItem={({ item }) => { - const users = item.keyRecipients; - if (users.length === 1) { - return ( - - ); - } else { - return ( - - ); - } - }} - /> - ); - } + return ( + item.key} + renderItem={({ item }) => { + const users = item.keyRecipients; + if (users.length === 1) { + return ( + + ); + } else { + return ( + + ); + } + }} + /> + ); } From 16ec0dc5606118064af370fae9f570118c1d4127 Mon Sep 17 00:00:00 2001 From: Wesley Aptekar-Cassels Date: Thu, 29 Apr 2021 15:23:50 +0800 Subject: [PATCH 4/8] PmConversationList [nfc]: Get Dispatch via useDispatch. --- src/pm-conversations/PmConversationList.js | 14 ++++++++------ src/pm-conversations/PmConversationsScreen.js | 5 ++--- src/unread/UnreadCards.js | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pm-conversations/PmConversationList.js b/src/pm-conversations/PmConversationList.js index 849637769be..09b8dc4d063 100644 --- a/src/pm-conversations/PmConversationList.js +++ b/src/pm-conversations/PmConversationList.js @@ -1,8 +1,9 @@ /* @flow strict-local */ import React, { useCallback } from 'react'; import { FlatList } from 'react-native'; +import { useDispatch } from '../react-redux'; -import type { Dispatch, PmConversationData, UserOrBot } from '../types'; +import type { PmConversationData, UserOrBot } from '../types'; import { createStyleSheet } from '../styles'; import { type PmKeyUsers } from '../utils/recipient'; import { pm1to1NarrowFromUser, pmNarrowFromUsers } from '../utils/narrow'; @@ -18,7 +19,6 @@ const styles = createStyleSheet({ }); type Props = $ReadOnly<{| - dispatch: Dispatch, conversations: PmConversationData[], |}>; @@ -26,18 +26,20 @@ type Props = $ReadOnly<{| * A list describing all PM conversations. * */ export default function PmConversationList(props: Props) { + const dispatch = useDispatch(); + const handleUserNarrow = useCallback( (user: UserOrBot) => { - props.dispatch(doNarrow(pm1to1NarrowFromUser(user))); + dispatch(doNarrow(pm1to1NarrowFromUser(user))); }, - [props.dispatch], + [dispatch], ); const handleGroupNarrow = useCallback( (users: PmKeyUsers) => { - props.dispatch(doNarrow(pmNarrowFromUsers(users))); + dispatch(doNarrow(pmNarrowFromUsers(users))); }, - [props.dispatch], + [dispatch], ); const { conversations } = props; diff --git a/src/pm-conversations/PmConversationsScreen.js b/src/pm-conversations/PmConversationsScreen.js index 71374382e77..b31e3f1fd5d 100644 --- a/src/pm-conversations/PmConversationsScreen.js +++ b/src/pm-conversations/PmConversationsScreen.js @@ -7,7 +7,7 @@ import type { RouteProp } from '../react-navigation'; import type { MainTabsNavigationProp } from '../main/MainTabsScreen'; import * as NavigationService from '../nav/NavigationService'; import { ThemeContext, createStyleSheet } from '../styles'; -import { useSelector, useDispatch } from '../react-redux'; +import { useSelector } from '../react-redux'; import { Label, ZulipButton, LoadingBanner } from '../common'; import { IconPeople, IconSearch } from '../common/Icons'; import PmConversationList from './PmConversationList'; @@ -43,7 +43,6 @@ type Props = $ReadOnly<{| * */ export default function PmConversationsScreen(props: Props) { const conversations = useSelector(getRecentConversations); - const dispatch = useDispatch(); const context = useContext(ThemeContext); return ( @@ -72,7 +71,7 @@ export default function PmConversationsScreen(props: Props) { {conversations.length === 0 ? (