From 1ffd58b92a57e57e8985a22156b1fc1837de1565 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 16 Nov 2023 11:00:34 -0800 Subject: [PATCH 01/14] test: Rename "metatests.js" so that it actually runs Is it possible that this test file has never actually been getting run by our test suite? Yikes. Now it does. --- src/__tests__/{metatests.js => test-test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{metatests.js => test-test.js} (100%) diff --git a/src/__tests__/metatests.js b/src/__tests__/test-test.js similarity index 100% rename from src/__tests__/metatests.js rename to src/__tests__/test-test.js From c193dbfb5b336f5bc48e5d8a7d669619868c44a8 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 16 Nov 2023 10:50:03 -0800 Subject: [PATCH 02/14] test: Fix RN Jest setup to work in jsdom on recent Node releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch is a backport of commit facebook/react-native@cf631ad59 from upstream. I've also pushed it to a branch in our fork: https://github.com/zulip/react-native/compare/v0.68.7...0.68.7-zulip In particular this avoids a symptom which I otherwise see on Node v18.16.1: ``` FAIL src/webview/__tests__/generateInboundEventEditSequence-test.js ● Test suite failed to run TypeError: Cannot redefine property: performance at Object. (node_modules/react-native/jest/setup.js:20:20) ``` The symptom affects three of our test files: src/__tests__/test-test.js src/webview/__tests__/generateInboundEventEditSequence-test.js src/webview/js/__tests__/rewriteHtml-test.js because those are the three that have `@jest-environment` annotations calling for "jsdom" or related environments. --- patches/react-native+0.68.7.patch | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/patches/react-native+0.68.7.patch b/patches/react-native+0.68.7.patch index 024a73247cc..fe4cc5f9760 100644 --- a/patches/react-native+0.68.7.patch +++ b/patches/react-native+0.68.7.patch @@ -1,3 +1,76 @@ +diff --git a/node_modules/react-native/jest/setup.js b/node_modules/react-native/jest/setup.js +index 5bc654475..e61bbf49c 100644 +--- a/node_modules/react-native/jest/setup.js ++++ b/node_modules/react-native/jest/setup.js +@@ -15,22 +15,52 @@ const mockComponent = jest.requireActual('./mockComponent'); + jest.requireActual('@react-native/polyfills/Object.es8'); + jest.requireActual('@react-native/polyfills/error-guard'); + +-global.__DEV__ = true; +- +-global.performance = { +- now: jest.fn(Date.now), +-}; +- +-global.Promise = jest.requireActual('promise'); +-global.regeneratorRuntime = jest.requireActual('regenerator-runtime/runtime'); +-global.window = global; +- +-global.requestAnimationFrame = function (callback) { +- return setTimeout(callback, 0); +-}; +-global.cancelAnimationFrame = function (id) { +- clearTimeout(id); +-}; ++Object.defineProperties(global, { ++ __DEV__: { ++ configurable: true, ++ enumerable: true, ++ value: true, ++ writable: true, ++ }, ++ Promise: { ++ configurable: true, ++ enumerable: true, ++ value: jest.requireActual('promise'), ++ writable: true, ++ }, ++ cancelAnimationFrame: { ++ configurable: true, ++ enumerable: true, ++ value: id => clearTimeout(id), ++ writable: true, ++ }, ++ performance: { ++ configurable: true, ++ enumerable: true, ++ value: { ++ now: jest.fn(Date.now), ++ }, ++ writable: true, ++ }, ++ regeneratorRuntime: { ++ configurable: true, ++ enumerable: true, ++ value: jest.requireActual('regenerator-runtime/runtime'), ++ writable: true, ++ }, ++ requestAnimationFrame: { ++ configurable: true, ++ enumerable: true, ++ value: callback => setTimeout(callback, 0), ++ writable: true, ++ }, ++ window: { ++ configurable: true, ++ enumerable: true, ++ value: global, ++ writable: true, ++ }, ++}); + + // there's a __mock__ for it. + jest.setMock( diff --git a/node_modules/react-native/scripts/react_native_pods.rb b/node_modules/react-native/scripts/react_native_pods.rb index f2ceeda..c618f77 100644 --- a/node_modules/react-native/scripts/react_native_pods.rb From 109d0e291fe3185e391db0da035d008c3ac3fe1f Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 16:10:26 -0800 Subject: [PATCH 03/14] deps: Update icons font for our current `@zulip/shared` 0.0.18 Done by rerunning tools/build-icon-font . Ideally this should have happened at the same time as upgrading `@zulip/shared`, so in commit 9716c9e0e and/or previous upgrades. --- static/assets/fonts/zulip-icons.map.js | 5 ++++- static/assets/fonts/zulip-icons.ttf | Bin 3100 -> 3988 bytes 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/static/assets/fonts/zulip-icons.map.js b/static/assets/fonts/zulip-icons.map.js index 28b9b38e32a..bf692aceaa6 100644 --- a/static/assets/fonts/zulip-icons.map.js +++ b/static/assets/fonts/zulip-icons.map.js @@ -8,5 +8,8 @@ export default { "bot": 61697, "ellipsis-v-solid": 61698, "gif": 61699, - "globe": 61700 + "globe": 61700, + "language": 61701, + "mute": 61702, + "readreceipts": 61703 }; diff --git a/static/assets/fonts/zulip-icons.ttf b/static/assets/fonts/zulip-icons.ttf index 710f9e0fa4dc3c64f43a0ae788c0996328b880f5..8d6fb46cd63d9aa27b1844a8e2b6e4c032ad379e 100644 GIT binary patch delta 1735 zcma)7&u<%55T1GV?s{$4>-F#0PMXHCodQiA+wnFnO{6q}sv#<(ftIR>Zc;a?uv3#d zrIjiY)e90Rcj^5)$GDhl&G2AW&%~IOI@3fE>QJ-9Lb}zWLtF zn|U*D-h1-p)TON*Vj>FAI(ew{+Uz0r z67)vn^kT=(y^)_MQvQNrSJOV%;tD-L0WxTC#ctNW{Qj3WiGr6Aw7YfsOuH1la)&7N z9p*ok#S09i+}zy!S9qQ6I2KoA91HyFPyhDbi^k&*C?F&FsQ&Zk>qPJN0)1W~d*E|d z!W^^=-KS7@h!;EWlS&a9pcy(!ZF&u}GKq(D@;hTeVG826g+gRN8qiI^fQPh394`gy z$0k^(OsvjME>fq%^LSCJ)&41ubnYH$i5FhYIcG$7OYk%X~~3=U$^ z6lMv5=ezfKinZ>DDCu&uvN@uA%kxb49TD!_K#csZA)OpO2KHmuHxPLNddO*oN;_zY zPEm`_ZfKjEuHzRAH@*GSlvbiHU`_6fX^W=hbOT$>){MymzoP-TTO)^Ybu0 z=S)88x^@sSHZxn9Tc6vvZ*F$Rn0rBr3bbiy#>JKwQF!?_E*UO>8IL9Qa57P@jhod` zHaIt==4*+qY}E#!B@-du$(lNf$5AZQ%-|7QPnl*)w~qu(KA=TZ%hI!&-?IE#R=2E< zKdS{S%O`lx*`R6r%pmXI5@y{tH^}KQ${wF*=+zjO=g$G!O3mbN%|=yLYq3_QS?WzSz6^d!K%l4G$|`53@%I z;TLQwUe=i}x@%ye;c$O`d!gSj`U~6h{o!v%Mn=BMgo}s53pbzFj|{N9%bPQJ_MoO{ka=iGBH?@8uax(fjy ziY*w(UnyL_bnHuR77zrl77Eid3*Y8%(eBe;E;hVYwy>Q5ur32; zx>oi|?S5~Yv0urV8XYF&2>0*FM6J>8*uBAkxs1ovdb8N|+)8|p1;**fHM~wsWN^R> z)3meeUZebJzyAP;EHUX^tGUs>w*L7AJATUZd%Ahajh22{Ci><-k@7wCp1L!ax#?aw zKvZXl#mJ8jTfpN#(NRd96b>p4OQi4K2>E-71R`9A_DSPHlTQE~W)*|`DXgH1Rn*Z!8=JpO{UpA(a83(FPNv-n7_tx zlndh~|GC6d= From e99f28427cce869125f5da837668d56eb747f37c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 16:06:22 -0800 Subject: [PATCH 04/14] tools [nfc]: Factor out listSvgFiles in build-icon-font --- tools/build-icon-font | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/build-icon-font b/tools/build-icon-font index 13b19d994c3..e9bb50d87a4 100755 --- a/tools/build-icon-font +++ b/tools/build-icon-font @@ -46,12 +46,16 @@ const fontName = 'zulip-icons'; // The root of our tree. const rootDir = path.dirname(__dirname); +function listSvgFiles(dirPath) { + return fs + .readdirSync(dirPath) + .filter(name => name.endsWith('.svg')) + .map(name => path.join(dirPath, name)); +} + async function main() { const srcDir = path.join(packagePath('@zulip/shared'), 'icons'); - const iconFiles = fs - .readdirSync(srcDir) - .filter(name => name.endsWith('.svg')) - .map(name => path.join(srcDir, name)); + const iconFiles = listSvgFiles(srcDir); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'build-icon-font-')); From eadbab00d4ab38d7198fb01892bdd1ca99a6cc9b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 16:08:21 -0800 Subject: [PATCH 05/14] icons: Add the Zulip "follow" icon This SVG file is copied verbatim from the web app. Ideally we'd do that by upgrading `@zulip/shared`... but now that this codebase is legacy and we're spending less time maintaining it, we've accumulated a backlog of changes there that are incompatible and require adjustments in our code. So the expedient thing is to just take the individual icon we currently need and pull it into our tree directly. Moreover, we want this icon inside the webview. This makes only the second Zulip icon we've put inside the webview, after the "play_button" icon we added just the other day; and I wouldn't say we've developed a good streamlined pattern for it. But for this legacy codebase, we'll content ourselves with just following the pattern set by "play_button". That also means duplicating the icon's SVG file into two places. Awkward, but at least it's not any kind of space-consumption issue: the file is under a kilobyte in size. --- src/common/Icons.js | 3 ++- src/webview/static/images/follow.svg | 3 +++ static/assets/fonts/zulip-icons.map.js | 3 ++- static/assets/fonts/zulip-icons.ttf | Bin 3988 -> 4216 bytes static/icons/follow.svg | 3 +++ tools/build-icon-font | 6 ++++-- tools/build-webview | 1 + 7 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/webview/static/images/follow.svg create mode 100644 static/icons/follow.svg diff --git a/src/common/Icons.js b/src/common/Icons.js index 8b2aef67920..1afd96793ee 100644 --- a/src/common/Icons.js +++ b/src/common/Icons.js @@ -117,5 +117,6 @@ export const IconAlertTriangle: SpecificIconType = makeIcon(Feather, 'alert-tria // WildcardMentionItem depends on this being square. export const IconWildcardMention: SpecificIconType = makeIcon(FontAwesome, 'bullhorn'); -// eslint-disable-next-line react/function-component-definition +/* eslint-disable react/function-component-definition */ export const IconWebPublic: SpecificIconType = props => ; +export const IconFollow: SpecificIconType = props => ; diff --git a/src/webview/static/images/follow.svg b/src/webview/static/images/follow.svg new file mode 100644 index 00000000000..3cbded1e623 --- /dev/null +++ b/src/webview/static/images/follow.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/assets/fonts/zulip-icons.map.js b/static/assets/fonts/zulip-icons.map.js index bf692aceaa6..a164f0de1f5 100644 --- a/static/assets/fonts/zulip-icons.map.js +++ b/static/assets/fonts/zulip-icons.map.js @@ -11,5 +11,6 @@ export default { "globe": 61700, "language": 61701, "mute": 61702, - "readreceipts": 61703 + "readreceipts": 61703, + "follow": 61704 }; diff --git a/static/assets/fonts/zulip-icons.ttf b/static/assets/fonts/zulip-icons.ttf index 8d6fb46cd63d9aa27b1844a8e2b6e4c032ad379e..a667b70e50777c8c1c649e1d5786e346dbae9adc 100644 GIT binary patch delta 1151 zcma)*%TE(Q9LImN-R^E*+tO|yP+AGaf;?NqLJ%bpgB(oI5RDp4s61L?OB<;aFO(Q# zOgxb_>cNu-^`KF~g9i?HFfoRc7cU+(;lRNQ2NPp_eP^9Nz}bBIo&C*kevjGdoqgY% zYQ%`hMXO|^)QO2R$6D^UJtJZRz&Ae8(>wm<>y#cX;0=g2mZtr7Thg==%a zQXf``lmd~ec--!1F@0CJn`bhS`cE=6^ zq+l2-W|zzC5Pd`P2;Q3(vc=iApLT8&c`Dd6SS~GAy3ZXSA@aV!`n$EwgePWofG*Cr zzb~$RO|zI8XSv!dQpunu8l_2EqFZzqZZfcu6YcW3L_YH1(aDRM4(iAceDL7bfaPT; zjY1SgtO(Q~H})y;lgSP*CploT;n68*`D<9k{xNH@(K0D;jv%SqdQ+ipgbGjtGzxeD zE~I5BnTPWrO_1ubF4J{$KpJM6xhxGa$xO>eb|YuK3Bxwh-z2NEy-m*mzCNSvhjg?LtTP8XjTw{ zUJ^K<(}E~;Mi79`3f#~+K@ge~7|?kE>LJ`qP7RF_6$SOs1wj~E65!ZyK>zSUuL$@B zUKIGCxQiAp=v4u~uS)_yv?@TudyLG-_S0i~I5-YJKx1$1@$!5z4n-R+NA7x3^P@0o{ncfhq z+5I}RC)?VRsut1OL*a1fp=x-T8Z(TT>NV8;(P*_fok*mc*{;f5ex)m4&UZP&vZHsf zacFgDuvO6mYHa*eTs3s2w&rvBNFtF+BsLznFG~0${?Icri?O(w@*Y-|xl*A}s+yO* JNtxkz{sMqln@s=! delta 925 zcma)*y=zlZ7{;G_lH7dVBsbq_3T+`OqLxNeLa~;DC^{6Qv@X)YG)jYwcc8>Uj6ya*iG8CTB9^p%-lOX1-K5_F;XwC zG>sg7Z~`i5V4+wqzy9>~K43j!(CKDlv6YQJ{s06&a{R@{ZYI#%T>)27zP$f%WB;Vl zEBlN}`w2`$(1&xlh8FJ7$w?gW!q;sJ*s%Bp5JZUI5bZDt(L?n{x?V1L5Jv)0B&ktu zl<4Ml!bLAHNBzv{pnt6EZ?Z~~75u|p(@iHo10(36rim)&V-bUCRItUQDUO1Ht8zk& z8*aHS&d4ceLH0ZB+%`Scv&AB+b%N^S_H5ICjdqY()RYEM!91E+?zrWQCP=-ZQTx8A2~k@b?l6`#N$RpD40L!33UwMC0r}Z=euNfy o&Fd61!yHhjk=FU=JTJX}o59Ot0kIGB-5%qWd1$GO2*#H0l diff --git a/static/icons/follow.svg b/static/icons/follow.svg new file mode 100644 index 00000000000..3cbded1e623 --- /dev/null +++ b/static/icons/follow.svg @@ -0,0 +1,3 @@ + + + diff --git a/tools/build-icon-font b/tools/build-icon-font index e9bb50d87a4..ee611adfbb7 100755 --- a/tools/build-icon-font +++ b/tools/build-icon-font @@ -54,8 +54,10 @@ function listSvgFiles(dirPath) { } async function main() { - const srcDir = path.join(packagePath('@zulip/shared'), 'icons'); - const iconFiles = listSvgFiles(srcDir); + const iconFiles = [ + ...listSvgFiles(path.join(packagePath('@zulip/shared'), 'icons')), + ...listSvgFiles(path.join(rootDir, 'static/icons')), + ]; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'build-icon-font-')); diff --git a/tools/build-webview b/tools/build-webview index f5735f3f051..766166d45fe 100755 --- a/tools/build-webview +++ b/tools/build-webview @@ -194,6 +194,7 @@ sync "src/webview/static" "${dest}" < Date: Wed, 15 Nov 2023 20:04:48 -0800 Subject: [PATCH 06/14] follow-topic: Add getter isTopicFollowed --- src/mute/muteModel.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/mute/muteModel.js b/src/mute/muteModel.js index 9e0b846832e..82aade0293e 100644 --- a/src/mute/muteModel.js +++ b/src/mute/muteModel.js @@ -95,6 +95,20 @@ export function isTopicVisible( } } +/** + * Whether the user is following this topic. + */ +export function isTopicFollowed(streamId: number, topic: string, mute: MuteState): boolean { + switch (getTopicVisibilityPolicy(mute, streamId, topic)) { + case UserTopicVisibilityPolicy.None: + case UserTopicVisibilityPolicy.Muted: + case UserTopicVisibilityPolicy.Unmuted: + return false; + case UserTopicVisibilityPolicy.Followed: + return true; + } +} + // // // Reducer. From ce3b105c3f3445e85cc7d17fe44193ec0bf10d94 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 16:48:18 -0800 Subject: [PATCH 07/14] follow-topic: Show "follow" icon in app bar of topic message list --- src/title/TitleStream.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/title/TitleStream.js b/src/title/TitleStream.js index 6741fe70c6b..0d97071b060 100644 --- a/src/title/TitleStream.js +++ b/src/title/TitleStream.js @@ -11,7 +11,7 @@ import type { Narrow } from '../types'; import styles, { createStyleSheet } from '../styles'; import { useSelector, useDispatch } from '../react-redux'; import StreamIcon from '../streams/StreamIcon'; -import { isTopicNarrow, topicOfNarrow } from '../utils/narrow'; +import { isTopicNarrow, streamIdOfNarrow, topicOfNarrow } from '../utils/narrow'; import { getAuth, getFlags, @@ -22,12 +22,13 @@ import { getSettings, getZulipFeatureLevel, } from '../selectors'; -import { getMute } from '../mute/muteModel'; +import { getMute, isTopicFollowed } from '../mute/muteModel'; import { showStreamActionSheet, showTopicActionSheet } from '../action-sheets'; import type { ShowActionSheetWithOptions } from '../action-sheets'; import { getUnread } from '../unread/unreadModel'; import { getOwnUserRole } from '../permissionSelectors'; import { useNavigation } from '../react-navigation'; +import { IconFollow } from '../common/Icons'; type Props = $ReadOnly<{| narrow: Narrow, @@ -46,6 +47,14 @@ const componentStyles = createStyleSheet({ flexDirection: 'row', alignItems: 'center', }, + topicRow: { + flexDirection: 'row', + alignItems: 'center', + }, + followIcon: { + paddingLeft: 4, + opacity: 0.4, + }, }); export default function TitleStream(props: Props): Node { @@ -70,6 +79,12 @@ export default function TitleStream(props: Props): Node { useActionSheet().showActionSheetWithOptions; const _ = useContext(TranslationContext); + const isFollowed = useSelector( + state => + isTopicNarrow(narrow) + && isTopicFollowed(streamIdOfNarrow(narrow), topicOfNarrow(narrow), getMute(state)), + ); + return ( {isTopicNarrow(narrow) && ( - - {topicOfNarrow(narrow)} - + + + {topicOfNarrow(narrow)} + + {isFollowed && ( + + )} + )} From 584695ab5bb8c66f46c17817be3b1f1c95bd1eac Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 16:22:46 -0800 Subject: [PATCH 08/14] streams [nfc]: Unpack padding logic in StreamItem --- src/streams/StreamItem.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/streams/StreamItem.js b/src/streams/StreamItem.js index e9ccef1a7cf..dbf350df1e1 100644 --- a/src/streams/StreamItem.js +++ b/src/streams/StreamItem.js @@ -125,9 +125,9 @@ export default function StreamItem(props: Props): Node { wrapper: { flexDirection: 'row', alignItems: 'center', - ...(handleExpandCollapse - ? { paddingRight: 16 } - : { paddingVertical: 8, paddingHorizontal: 16 }), + paddingVertical: handleExpandCollapse ? 0 : 8, + paddingLeft: handleExpandCollapse ? 0 : 16, + paddingRight: 16, backgroundColor, opacity: isMuted ? 0.5 : 1, }, From d34c8149e29067ae273eccceedb641c99552dced Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 16:29:41 -0800 Subject: [PATCH 09/14] styles [nfc]: Convert to in-render-function styles for PmConversationList --- src/pm-conversations/PmConversationList.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pm-conversations/PmConversationList.js b/src/pm-conversations/PmConversationList.js index 5ae0d57ed5d..5c4cb88c09b 100644 --- a/src/pm-conversations/PmConversationList.js +++ b/src/pm-conversations/PmConversationList.js @@ -1,11 +1,10 @@ /* @flow strict-local */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { Node } from 'react'; import { FlatList } from 'react-native'; import { useDispatch, useSelector } from '../react-redux'; import type { PmConversationData, UserOrBot } from '../types'; -import { createStyleSheet } from '../styles'; import { type PmKeyUsers } from '../utils/recipient'; import { pm1to1NarrowFromUser, pmNarrowFromUsers } from '../utils/narrow'; import UserItem from '../users/UserItem'; @@ -13,13 +12,6 @@ import GroupPmConversationItem from './GroupPmConversationItem'; import { doNarrow } from '../actions'; import { getMutedUsers } from '../selectors'; -const styles = createStyleSheet({ - list: { - flex: 1, - flexDirection: 'column', - }, -}); - type Props = $ReadOnly<{| conversations: $ReadOnlyArray, |}>; @@ -47,6 +39,16 @@ export default function PmConversationList(props: Props): Node { const { conversations } = props; const mutedUsers = useSelector(getMutedUsers); + const styles = useMemo( + () => ({ + list: { + flex: 1, + flexDirection: 'column', + }, + }), + [], + ); + return ( Date: Wed, 15 Nov 2023 16:32:38 -0800 Subject: [PATCH 10/14] follow-topic: Add "follow" icon on inbox and topic-list screens This is most visible on the inbox screen, the app's home screen. It also appears on the screen that lists the topics for a stream. --- src/pm-conversations/PmConversationList.js | 6 ++++-- src/streams/StreamItem.js | 6 ++++-- src/streams/TopicItem.js | 20 ++++++++++++++++++-- src/unread/UnreadCards.js | 3 ++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/pm-conversations/PmConversationList.js b/src/pm-conversations/PmConversationList.js index 5c4cb88c09b..80921cea634 100644 --- a/src/pm-conversations/PmConversationList.js +++ b/src/pm-conversations/PmConversationList.js @@ -14,6 +14,7 @@ import { getMutedUsers } from '../selectors'; type Props = $ReadOnly<{| conversations: $ReadOnlyArray, + extraPaddingEnd?: number, |}>; /** @@ -36,7 +37,7 @@ export default function PmConversationList(props: Props): Node { [dispatch], ); - const { conversations } = props; + const { conversations, extraPaddingEnd = 0 } = props; const mutedUsers = useSelector(getMutedUsers); const styles = useMemo( @@ -44,9 +45,10 @@ export default function PmConversationList(props: Props): Node { list: { flex: 1, flexDirection: 'column', + paddingRight: extraPaddingEnd, }, }), - [], + [extraPaddingEnd], ); return ( diff --git a/src/streams/StreamItem.js b/src/streams/StreamItem.js index dbf350df1e1..2249b733a2d 100644 --- a/src/streams/StreamItem.js +++ b/src/streams/StreamItem.js @@ -44,6 +44,7 @@ type Props = $ReadOnly<{| unreadCount?: number, iconSize: number, offersSubscribeButton?: boolean, + extraPaddingEnd?: number, // These stream names are here for a mix of good reasons and (#3918) bad ones. // To audit all uses, change `name` to write-only (`-name:`), and run Flow. onPress: ({ stream_id: number, name: string, ... }) => void, @@ -87,6 +88,7 @@ export default function StreamItem(props: Props): Node { iconSize, offersSubscribeButton = false, unreadCount, + extraPaddingEnd = 0, onPress, onSubscribeButtonPressed, } = props; @@ -127,7 +129,7 @@ export default function StreamItem(props: Props): Node { alignItems: 'center', paddingVertical: handleExpandCollapse ? 0 : 8, paddingLeft: handleExpandCollapse ? 0 : 16, - paddingRight: 16, + paddingRight: extraPaddingEnd + 16, backgroundColor, opacity: isMuted ? 0.5 : 1, }, @@ -156,7 +158,7 @@ export default function StreamItem(props: Props): Node { fontSize: 12, }, }), - [backgroundColor, handleExpandCollapse, isMuted, textColor], + [backgroundColor, extraPaddingEnd, handleExpandCollapse, isMuted, textColor], ); const collapseButton = handleExpandCollapse && ( diff --git a/src/streams/TopicItem.js b/src/streams/TopicItem.js index 37e5f469a8e..ef01f72e83a 100644 --- a/src/streams/TopicItem.js +++ b/src/streams/TopicItem.js @@ -6,7 +6,7 @@ import { View } from 'react-native'; import { useActionSheet } from '@expo/react-native-action-sheet'; import styles, { BRAND_COLOR, createStyleSheet } from '../styles'; -import { IconMention } from '../common/Icons'; +import { IconMention, IconFollow } from '../common/Icons'; import ZulipText from '../common/ZulipText'; import Touchable from '../common/Touchable'; import UnreadCount from '../common/UnreadCount'; @@ -22,10 +22,11 @@ import { getOwnUser, getZulipFeatureLevel, } from '../selectors'; -import { getMute } from '../mute/muteModel'; +import { getMute, isTopicFollowed } from '../mute/muteModel'; import { getUnread } from '../unread/unreadModel'; import { getOwnUserRole } from '../permissionSelectors'; import { useNavigation } from '../react-navigation'; +import { ThemeContext } from '../styles/theme'; const componentStyles = createStyleSheet({ selectedRow: { @@ -44,6 +45,11 @@ const componentStyles = createStyleSheet({ muted: { opacity: 0.5, }, + followedIcon: { + paddingLeft: 4, + width: 20, + opacity: 0.2, + }, }); type Props = $ReadOnly<{| @@ -84,6 +90,10 @@ export default function TopicItem(props: Props): Node { zulipFeatureLevel: getZulipFeatureLevel(state), })); + const theme = useContext(ThemeContext); + const iconColor = theme.themeName === 'dark' ? 'white' : 'black'; + const isFollowed = useSelector(state => isTopicFollowed(streamId, name, getMute(state))); + return ( onPress(streamId, name)} @@ -112,6 +122,12 @@ export default function TopicItem(props: Props): Node { /> {isMentioned && } + {isFollowed ? ( + + ) : ( + // $FlowFixMe[incompatible-type]: complains about `color` but that's not present + + )} ); diff --git a/src/unread/UnreadCards.js b/src/unread/UnreadCards.js index d1633a039f6..45e10d0b7cb 100644 --- a/src/unread/UnreadCards.js +++ b/src/unread/UnreadCards.js @@ -93,6 +93,7 @@ export default function UnreadCards(props: Props): Node { isWebPublic={section.isWebPublic} backgroundColor={section.color} unreadCount={section.unread} + extraPaddingEnd={20} onPress={stream => { setTimeout(() => dispatch(doNarrow(streamNarrow(stream.stream_id)))); }} @@ -101,7 +102,7 @@ export default function UnreadCards(props: Props): Node { } renderItem={({ item, section }) => section.key === 'private' ? ( - + ) : ( Date: Wed, 15 Nov 2023 16:56:19 -0800 Subject: [PATCH 11/14] html: Accept boolean in `template`, as well as string and number As discussed back in 4851f34c8, the values get stringified either by `lodash.escape` or by the `join`. Stringifying a boolean works just as nicely as a number. --- src/webview/html/template.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webview/html/template.js b/src/webview/html/template.js index 87cf27a0a57..be6fcc32f93 100644 --- a/src/webview/html/template.js +++ b/src/webview/html/template.js @@ -16,7 +16,7 @@ import escape from 'lodash.escape'; */ export default ( strings: $ReadOnlyArray, - ...values: $ReadOnlyArray + ...values: $ReadOnlyArray ): string => { // $FlowIssue[prop-missing] #2616 github.com/facebook/flow/issues/2616 const raw: $ReadOnlyArray = strings.raw; From 4ef0a0a912fbbc88df452f539ecef95bc9dfb7e4 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 15 Nov 2023 20:01:16 -0800 Subject: [PATCH 12/14] follow-topic: Show "follow" icon in recipient headers Together with the preceding few commits which add this icon in some other places in the UI, this completes #5770. Fixes: #5770 --- ...oundEventEditSequence-test.js.snap.android | 120 ++++++++++++------ ...eInboundEventEditSequence-test.js.snap.ios | 120 ++++++++++++------ .../generateInboundEventEditSequence-test.js | 55 ++++++++ .../generateInboundEventEditSequence.js | 15 ++- src/webview/html/__tests__/header-test.js | 2 + src/webview/html/header.js | 8 +- src/webview/static/base.css | 12 ++ 7 files changed, 244 insertions(+), 88 deletions(-) diff --git a/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.android b/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.android index 50b5af73d9c..f2dda872427 100644 --- a/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.android +++ b/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.android @@ -68,6 +68,8 @@ exports[`getEditSequence correct for interesting changes within a given message exports[`getEditSequence correct for interesting changes within a given message add/remove/change emoji status status emoji: zulip extra -> zulip extra (no change) 1`] = `0`; +exports[`getEditSequence correct for interesting changes within a given message follow a topic 1`] = `1`; + exports[`getEditSequence correct for interesting changes within a given message mute a sender 1`] = `1`; exports[`getEditSequence correct for interesting changes within a given message polls choice added 1`] = `3`; @@ -78,6 +80,8 @@ exports[`getEditSequence correct for interesting changes within a given message exports[`getEditSequence correct for interesting changes within a given message star a message 1`] = `1`; +exports[`getEditSequence correct for interesting changes within a given message unfollow a topic 1`] = `1`; + exports[`getEditSequence correct for interesting changes within a given message unmute a sender 1`] = `1`; exports[`getEditSequence correct for interesting changes within a given message unstar a message 1`] = `1`; @@ -92,7 +96,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1 -
topic 1
+
topic 1
Feb 5, 1995
@@ -136,7 +140,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -185,7 +189,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 19, 1995
@@ -209,7 +213,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 2
+
topic 2
Feb 19, 1995
@@ -241,7 +245,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -265,7 +269,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjI=\\"> # stream 2
-
topic 2
+
topic 2
Mar 5, 1995
@@ -297,7 +301,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -612,7 +616,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 5, 1995
@@ -692,7 +696,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 19, 1995
@@ -716,7 +720,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 2
+
topic 2
Feb 19, 1995
@@ -880,7 +884,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -904,7 +908,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjI=\\"> # stream 2
-
topic 2
+
topic 2
Mar 5, 1995
@@ -928,7 +932,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -1000,7 +1004,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -1526,6 +1530,38 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible all-pm
" `; +exports[`messages -> piece descriptors -> content HTML is stable/sensible other interesting cases (single messages) message in followed topic 1`] = ` +"
+
+ Dec 31, 1969 +
+
+
+ # stream 1 +
+
example topic
+
Dec 31, 1969
+
+
+ \\"Nonrandom +
+
+
+
+ Nonrandom name sender User +
+
11:59 PM
+
+

This is an example stream message.

+ + + +
+ +
" +`; + exports[`messages -> piece descriptors -> content HTML is stable/sensible other interesting cases (single messages) message in unsubscribed stream 1`] = ` "
@@ -1536,7 +1572,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1568,7 +1604,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1610,7 +1646,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1642,7 +1678,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1674,7 +1710,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1706,7 +1742,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1738,7 +1774,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1770,7 +1806,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1802,7 +1838,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1834,7 +1870,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1866,7 +1902,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1898,7 +1934,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1930,7 +1966,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1962,7 +1998,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1994,7 +2030,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2026,7 +2062,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2060,7 +2096,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2092,7 +2128,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2124,7 +2160,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2156,7 +2192,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2655,7 +2691,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Feb 5, 1995
-
topic 1
+
topic 1
Feb 5, 1995
@@ -2695,7 +2731,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Mar 5, 1995
-
topic 1
+
topic 1
Mar 5, 1995
@@ -2740,7 +2776,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Feb 19, 1995
-
topic 1
+
topic 1
Feb 19, 1995
@@ -2760,7 +2796,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream
-
topic 2
+
topic 2
Feb 19, 1995
@@ -2788,7 +2824,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Mar 5, 1995
-
topic 1
+
topic 1
Mar 5, 1995
@@ -2837,7 +2873,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Feb 5, 1995
-
topic 1
+
topic 1
Feb 5, 1995
@@ -2890,7 +2926,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream
-
topic 2
+
topic 2
Feb 19, 1995
@@ -2914,7 +2950,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Mar 5, 1995
-
topic 1
+
topic 1
Mar 5, 1995
diff --git a/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.ios b/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.ios index 50b5af73d9c..f2dda872427 100644 --- a/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.ios +++ b/src/webview/__tests__/__snapshots__/generateInboundEventEditSequence-test.js.snap.ios @@ -68,6 +68,8 @@ exports[`getEditSequence correct for interesting changes within a given message exports[`getEditSequence correct for interesting changes within a given message add/remove/change emoji status status emoji: zulip extra -> zulip extra (no change) 1`] = `0`; +exports[`getEditSequence correct for interesting changes within a given message follow a topic 1`] = `1`; + exports[`getEditSequence correct for interesting changes within a given message mute a sender 1`] = `1`; exports[`getEditSequence correct for interesting changes within a given message polls choice added 1`] = `3`; @@ -78,6 +80,8 @@ exports[`getEditSequence correct for interesting changes within a given message exports[`getEditSequence correct for interesting changes within a given message star a message 1`] = `1`; +exports[`getEditSequence correct for interesting changes within a given message unfollow a topic 1`] = `1`; + exports[`getEditSequence correct for interesting changes within a given message unmute a sender 1`] = `1`; exports[`getEditSequence correct for interesting changes within a given message unstar a message 1`] = `1`; @@ -92,7 +96,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 5, 1995
@@ -136,7 +140,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -185,7 +189,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 19, 1995
@@ -209,7 +213,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 2
+
topic 2
Feb 19, 1995
@@ -241,7 +245,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -265,7 +269,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjI=\\"> # stream 2
-
topic 2
+
topic 2
Mar 5, 1995
@@ -297,7 +301,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -612,7 +616,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 5, 1995
@@ -692,7 +696,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Feb 19, 1995
@@ -716,7 +720,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 2
+
topic 2
Feb 19, 1995
@@ -880,7 +884,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -904,7 +908,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjI=\\"> # stream 2
-
topic 2
+
topic 2
Mar 5, 1995
@@ -928,7 +932,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -1000,7 +1004,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible HOME_N background: #123456\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
topic 1
+
topic 1
Mar 5, 1995
@@ -1526,6 +1530,38 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible all-pm
" `; +exports[`messages -> piece descriptors -> content HTML is stable/sensible other interesting cases (single messages) message in followed topic 1`] = ` +"
+
+ Dec 31, 1969 +
+
+
+ # stream 1 +
+
example topic
+
Dec 31, 1969
+
+
+ \\"Nonrandom +
+
+
+
+ Nonrandom name sender User +
+
11:59 PM
+
+

This is an example stream message.

+ + + +
+ +
" +`; + exports[`messages -> piece descriptors -> content HTML is stable/sensible other interesting cases (single messages) message in unsubscribed stream 1`] = ` "
@@ -1536,7 +1572,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1568,7 +1604,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1610,7 +1646,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1642,7 +1678,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1674,7 +1710,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1706,7 +1742,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1738,7 +1774,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1770,7 +1806,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1802,7 +1838,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1834,7 +1870,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1866,7 +1902,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1898,7 +1934,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1930,7 +1966,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1962,7 +1998,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -1994,7 +2030,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2026,7 +2062,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2060,7 +2096,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2092,7 +2128,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2124,7 +2160,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2156,7 +2192,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible other background: hsl(0, 0%, 80%)\\" data-narrow=\\"c3RyZWFtOjE=\\"> # stream 1
-
example topic
+
example topic
Dec 31, 1969
@@ -2655,7 +2691,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Feb 5, 1995
-
topic 1
+
topic 1
Feb 5, 1995
@@ -2695,7 +2731,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Mar 5, 1995
-
topic 1
+
topic 1
Mar 5, 1995
@@ -2740,7 +2776,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Feb 19, 1995
-
topic 1
+
topic 1
Feb 19, 1995
@@ -2760,7 +2796,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream
-
topic 2
+
topic 2
Feb 19, 1995
@@ -2788,7 +2824,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Mar 5, 1995
-
topic 1
+
topic 1
Mar 5, 1995
@@ -2837,7 +2873,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Feb 5, 1995
-
topic 1
+
topic 1
Feb 5, 1995
@@ -2890,7 +2926,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream
-
topic 2
+
topic 2
Feb 19, 1995
@@ -2914,7 +2950,7 @@ exports[`messages -> piece descriptors -> content HTML is stable/sensible stream Mar 5, 1995
-
topic 1
+
topic 1
Mar 5, 1995
diff --git a/src/webview/__tests__/generateInboundEventEditSequence-test.js b/src/webview/__tests__/generateInboundEventEditSequence-test.js index 6882f4e53fb..96cc7bb9693 100644 --- a/src/webview/__tests__/generateInboundEventEditSequence-test.js +++ b/src/webview/__tests__/generateInboundEventEditSequence-test.js @@ -29,6 +29,8 @@ import getMessageListElements from '../../message/getMessageListElements'; import { getGlobalSettings } from '../../selectors'; import { getBackgroundData } from '../backgroundData'; import { randString } from '../../utils/misc'; +import { makeMuteState } from '../../mute/__tests__/mute-testlib'; +import { UserTopicVisibilityPolicy } from '../../api/modelTypes'; // Tell ESLint to recognize `check` as a helper function that runs // assertions. @@ -443,6 +445,23 @@ describe('messages -> piece descriptors -> content HTML is stable/sensible', () }); }); + test('message in followed topic', () => { + check({ + narrow: HOME_NARROW, + messages: [baseSingleMessage], + state: eg.reduxStatePlus({ + streams: [...eg.plusReduxState.streams, stream1], + subscriptions: [ + ...eg.plusReduxState.subscriptions, + eg.makeSubscription({ stream: stream1 }), + ], + mute: makeMuteState([ + [stream1, baseSingleMessage.subject, UserTopicVisibilityPolicy.Followed], + ]), + }), + }); + }); + describe('message with reactions', () => { describe('displayEmojiReactionUsers: false', () => { const state = eg.reduxStatePlus({ @@ -991,6 +1010,42 @@ describe('getEditSequence correct for interesting changes', () => { ); }); + test('follow a topic', () => { + const message = eg.streamMessage(); + check( + { + messages: [message], + state: eg.reduxStatePlus({ + mute: makeMuteState([]), + }), + }, + { + messages: [message], + state: eg.reduxStatePlus({ + mute: makeMuteState([[eg.stream, message.subject, UserTopicVisibilityPolicy.Followed]]), + }), + }, + ); + }); + + test('unfollow a topic', () => { + const message = eg.streamMessage(); + check( + { + messages: [message], + state: eg.reduxStatePlus({ + mute: makeMuteState([[eg.stream, message.subject, UserTopicVisibilityPolicy.Followed]]), + }), + }, + { + messages: [message], + state: eg.reduxStatePlus({ + mute: makeMuteState([]), + }), + }, + ); + }); + // TODO(#5208): We haven't settled how we want to track name/avatar test.todo("sender's name/avatar changed"); diff --git a/src/webview/generateInboundEventEditSequence.js b/src/webview/generateInboundEventEditSequence.js index c48576571d6..d0f96be4940 100644 --- a/src/webview/generateInboundEventEditSequence.js +++ b/src/webview/generateInboundEventEditSequence.js @@ -8,6 +8,7 @@ import { ensureUnreachable } from '../generics'; import type { BackgroundData } from './backgroundData'; import messageListElementHtml from './html/messageListElementHtml'; import { getUserStatusFromModel } from '../user-statuses/userStatusesCore'; +import { isTopicFollowed } from '../mute/muteModel'; const NODE_ENV = process.env.NODE_ENV; @@ -72,7 +73,19 @@ function doElementsDifferInterestingly( return !isEqual(oldElement, newElement); case 'header': // TODO(?): False positives on `.subsequentMessage.content` changes - return !isEqual(oldElement, newElement); + if (!isEqual(oldElement, newElement)) { + return true; + } + if (newElement.subsequentMessage?.type === 'stream') { + const message = newElement.subsequentMessage; + if ( + isTopicFollowed(message.stream_id, message.subject, oldBackgroundData.mute) + !== isTopicFollowed(message.stream_id, message.subject, newBackgroundData.mute) + ) { + return true; + } + } + return false; case 'message': { invariant(newElement.type === 'message', 'oldElement.type equals newElement.type'); diff --git a/src/webview/html/__tests__/header-test.js b/src/webview/html/__tests__/header-test.js index fc212f20cf3..99b4eea6afb 100644 --- a/src/webview/html/__tests__/header-test.js +++ b/src/webview/html/__tests__/header-test.js @@ -3,8 +3,10 @@ import * as eg from '../../../__tests__/lib/exampleData'; import header from '../header'; import type { BackgroundData } from '../../backgroundData'; +import { makeMuteState } from '../../../mute/__tests__/mute-testlib'; const backgroundData: BackgroundData = ({ + mute: makeMuteState([]), ownEmail: eg.selfUser.email, subscriptions: [eg.stream], streams: new Map([[eg.stream.stream_id, eg.stream]]), diff --git a/src/webview/html/header.js b/src/webview/html/header.js index 6733053ccc4..66bb506050c 100644 --- a/src/webview/html/header.js +++ b/src/webview/html/header.js @@ -19,6 +19,7 @@ import { streamNameOfStreamMessage, } from '../../utils/recipient'; import { base64Utf8Encode } from '../../utils/encoding'; +import { isTopicFollowed } from '../../mute/muteModel'; const renderTopic = message => // TODO: pin down if '' happens, and what its proper semantics are. @@ -32,7 +33,7 @@ const renderTopic = message => * This is a private helper of messageListElementHtml. */ export default ( - { ownUser, subscriptions }: BackgroundData, + { mute, ownUser, subscriptions }: BackgroundData, element: HeaderMessageListElement, ): string => { const { subsequentMessage: message, style: headerStyle } = element; @@ -41,6 +42,7 @@ export default ( const streamName = streamNameOfStreamMessage(message); const topicNarrowStr = keyFromNarrow(topicNarrow(message.stream_id, message.subject)); const topicHtml = renderTopic(message); + const isFollowed = isTopicFollowed(message.stream_id, message.subject, mute); if (headerStyle === 'topic+date') { return template`\ @@ -49,7 +51,7 @@ export default ( data-narrow="${base64Utf8Encode(topicNarrowStr)}" data-msg-id="${message.id}" > -
$!${topicHtml}
+
$!${topicHtml}
${humanDate(new Date(message.timestamp * 1000))}
`; } else if (headerStyle === 'full') { @@ -70,7 +72,7 @@ export default ( data-narrow="${base64Utf8Encode(streamNarrowStr)}"> # ${streamName}
-
$!${topicHtml}
+
$!${topicHtml}
${humanDate(new Date(message.timestamp * 1000))}
`; } else { diff --git a/src/webview/static/base.css b/src/webview/static/base.css index 731c97371e5..673b8c369c6 100644 --- a/src/webview/static/base.css +++ b/src/webview/static/base.css @@ -116,10 +116,22 @@ body { .topic-text { flex: 1; padding: 0 8px; + display: flex; + align-items: center; overflow: hidden; text-overflow: ellipsis; pointer-events: none; } +.topic-text[data-followed="true"]::after { + content: ""; + background-image: url("images/follow.svg"); + margin-left: 12px; + width: 17px; + height: 17px; + /* opacity: 0.2 on web, but that's with the pastel stream colors; + * 0.3 stands up better to our gray. */ + opacity: 0.3; +} .topic-date { opacity: 0.5; padding: 0 8px; From 66c8985edcdb28aac383e295b6bcd598dfabe0b3 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 17 Nov 2023 13:55:56 -0800 Subject: [PATCH 13/14] example data: Update recentZulip{Version,FeatureLevel} These are the current values on chat.zulip.org. --- src/__tests__/lib/exampleData.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/lib/exampleData.js b/src/__tests__/lib/exampleData.js index 32424d3a5a5..abc22401009 100644 --- a/src/__tests__/lib/exampleData.js +++ b/src/__tests__/lib/exampleData.js @@ -212,8 +212,8 @@ export const userStatusEmojiRealm: UserStatus['status_emoji'] = deepFreeze({ export const realm: URL = new URL('https://zulip.example.org'); /** These may be raised but should not be lowered. */ -export const recentZulipVersion: ZulipVersion = new ZulipVersion('6.0-dev-2191-gf56ce7a159'); -export const recentZulipFeatureLevel = 153; +export const recentZulipVersion: ZulipVersion = new ZulipVersion('8.0-dev-2894-g86100cdb4e'); +export const recentZulipFeatureLevel = 226; export const makeAccount = ( args: {| From 4410bdea115b67579ef02c7ed6ce59b0075f863b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 16 Nov 2023 17:44:31 -0800 Subject: [PATCH 14/14] follow-topic: Add follow/unfollow options to topic action sheet Fixes: #5771 --- .../__tests__/action-sheet-test.js | 15 ++++++ src/action-sheets/index.js | 48 ++++++++++++++++++- static/translations/messages_en.json | 4 ++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/action-sheets/__tests__/action-sheet-test.js b/src/action-sheets/__tests__/action-sheet-test.js index 8a8ab876222..3b396ae44b9 100644 --- a/src/action-sheets/__tests__/action-sheet-test.js +++ b/src/action-sheets/__tests__/action-sheet-test.js @@ -98,6 +98,21 @@ describe('constructTopicActionButtons', () => { expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Mute topic'); }); + test('show followTopic on muted topic', () => { + const mute = makeMuteState([[eg.stream, topic]]); + expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Follow topic'); + }); + + test('show followTopic', () => { + const mute = makeMuteState([]); + expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Follow topic'); + }); + + test('show unfollowTopic', () => { + const mute = makeMuteState([[eg.stream, topic, UserTopicVisibilityPolicy.Followed]]); + expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Unfollow topic'); + }); + test('show resolveTopic', () => { expect(titles({ ...eg.plusBackgroundData })).toContain('Resolve topic'); }); diff --git a/src/action-sheets/index.js b/src/action-sheets/index.js index b70255d83f1..65b5d5d17dc 100644 --- a/src/action-sheets/index.js +++ b/src/action-sheets/index.js @@ -319,6 +319,24 @@ const muteTopic = { }, }; +const followTopic = { + title: 'Follow topic', + errorMessage: 'Failed to follow topic', + action: async ({ auth, streamId, topic, zulipFeatureLevel }) => { + invariant(zulipFeatureLevel >= 219, 'Should only attempt to follow topic on FL 219+'); + await api.updateUserTopic(auth, streamId, topic, UserTopicVisibilityPolicy.Followed); + }, +}; + +const unfollowTopic = { + title: 'Unfollow topic', + errorMessage: 'Failed to unfollow topic', + action: async ({ auth, streamId, topic, zulipFeatureLevel }) => { + invariant(zulipFeatureLevel >= 219, 'Should only attempt to unfollow topic on FL 219+'); + await api.updateUserTopic(auth, streamId, topic, UserTopicVisibilityPolicy.None); + }, +}; + const copyLinkToTopic = { title: 'Copy link to topic', errorMessage: 'Failed to copy topic link', @@ -651,6 +669,11 @@ export const constructTopicActionButtons = (args: {| const sub = subscriptions.get(streamId); const streamMuted = !!sub && !sub.in_home_view; + // TODO(server-7.0): Simplify this condition away. + const supportsUnmutingTopics = zulipFeatureLevel >= 170; + // TODO(server-8.0): Simplify this condition away. + const supportsFollowingTopics = zulipFeatureLevel >= 219; + const buttons = []; const unreadCount = getUnreadCountForTopic(unread, streamId, topic); if (unreadCount > 0) { @@ -661,25 +684,46 @@ export const constructTopicActionButtons = (args: {| switch (getTopicVisibilityPolicy(mute, streamId, topic)) { case UserTopicVisibilityPolicy.Muted: buttons.push(unmuteTopic); + if (supportsFollowingTopics) { + buttons.push(followTopic); + } break; case UserTopicVisibilityPolicy.None: case UserTopicVisibilityPolicy.Unmuted: + buttons.push(muteTopic); + if (supportsFollowingTopics) { + buttons.push(followTopic); + } + break; case UserTopicVisibilityPolicy.Followed: buttons.push(muteTopic); + if (supportsFollowingTopics) { + buttons.push(unfollowTopic); + } break; } } else if (sub && streamMuted) { // Muted stream. - // TODO(server-7.0): Simplify this condition away. - if (zulipFeatureLevel >= 170) { + if (supportsUnmutingTopics) { switch (getTopicVisibilityPolicy(mute, streamId, topic)) { case UserTopicVisibilityPolicy.None: case UserTopicVisibilityPolicy.Muted: buttons.push(unmuteTopicInMutedStream); + if (supportsFollowingTopics) { + buttons.push(followTopic); + } break; case UserTopicVisibilityPolicy.Unmuted: + buttons.push(muteTopic); + if (supportsFollowingTopics) { + buttons.push(followTopic); + } + break; case UserTopicVisibilityPolicy.Followed: buttons.push(muteTopic); + if (supportsFollowingTopics) { + buttons.push(unfollowTopic); + } break; } } diff --git a/static/translations/messages_en.json b/static/translations/messages_en.json index c9adb956ca2..19796fd8879 100644 --- a/static/translations/messages_en.json +++ b/static/translations/messages_en.json @@ -170,6 +170,10 @@ "Delete topic": "Delete topic", "Deleting a topic will immediately remove it and its messages for everyone. Other users may find this confusing, especially if they had received an email or push notification related to the deleted messages.\n\nAre you sure you want to permanently delete “{topic}”?": "Deleting a topic will immediately remove it and its messages for everyone. Other users may find this confusing, especially if they had received an email or push notification related to the deleted messages.\n\nAre you sure you want to permanently delete “{topic}”?", "Unmute topic": "Unmute topic", + "Follow topic": "Follow topic", + "Failed to follow topic": "Failed to follow topic", + "Unfollow topic": "Unfollow topic", + "Failed to unfollow topic": "Failed to unfollow topic", "Mute stream": "Mute stream", "Unmute stream": "Unmute stream", "No Internet connection": "No Internet connection",