1
1
/* @flow strict-local */
2
+ import * as logging from '../utils/logging' ;
2
3
import * as NavigationService from '../nav/NavigationService' ;
3
4
import type { Narrow , ThunkAction } from '../types' ;
4
- import { getAuth } from '../selectors' ;
5
+ import { getAuth , getRealm , getMessages , getZulipFeatureLevel } from '../selectors' ;
5
6
import { getNearOperandFromLink , getNarrowFromLink } from '../utils/internalLinks' ;
6
7
import { openLinkWithUserPreference } from '../utils/openLink' ;
7
8
import { navigateToChat } from '../nav/navActions' ;
@@ -10,6 +11,16 @@ import { getStreamsById, getStreamsByName } from '../subscriptions/subscriptionS
10
11
import * as api from '../api' ;
11
12
import { isUrlOnRealm } from '../utils/url' ;
12
13
import { getOwnUserId } from '../users/userSelectors' ;
14
+ import {
15
+ isTopicNarrow ,
16
+ isStreamNarrow ,
17
+ topicNarrow ,
18
+ streamIdOfNarrow ,
19
+ topicOfNarrow ,
20
+ streamNarrow ,
21
+ caseNarrowDefault ,
22
+ } from '../utils/narrow' ;
23
+ import { hasMessageEverBeenInStream , hasMessageEverHadTopic } from './messageSelectors' ;
13
24
14
25
/**
15
26
* Navigate to the given narrow.
@@ -21,6 +32,171 @@ export const doNarrow =
21
32
NavigationService . dispatch ( navigateToChat ( narrow ) ) ;
22
33
} ;
23
34
35
+ /**
36
+ * Narrow to a /near/ link, possibly after reinterpreting it for a message move.
37
+ *
38
+ * It feels quite broken when a link is clearly meant to get you to a
39
+ * specific message, but tapping it brings you to a narrow where the message
40
+ * *used* to be but isn't anymore because it was moved to a new stream or
41
+ * topic. This was #5306.
42
+ *
43
+ * This action, when it can, recognizes when that's about to happen and
44
+ * instead narrows you to the message's current stream/topic.
45
+ *
46
+ * To do so, it obviously needs to know the message's current stream/topic.
47
+ * If those can't be gotten from Redux, we ask the server. If the server
48
+ * can't help us (gives an error), we can't help the user, so we won't
49
+ * follow a move in that case.
50
+ *
51
+ * N.B.: Gives a bad experience when the request takes a long time. We
52
+ * should fix that; see TODOs.
53
+ */
54
+ const doNarrowNearLink =
55
+ ( narrow : Narrow , nearOperand : number ) : ThunkAction < Promise < void >> =>
56
+ async ( dispatch , getState ) => {
57
+ const state = getState ( ) ;
58
+
59
+ const auth = getAuth ( state ) ;
60
+ const messages = getMessages ( state ) ;
61
+ const zulipFeatureLevel = getZulipFeatureLevel ( state ) ;
62
+ const allowEditHistory = getRealm ( state ) . allowEditHistory ;
63
+
64
+ /**
65
+ * Narrow to the /near/ link without reinterpreting it for a message move.
66
+ *
67
+ * Use this when the link is meant to find the specific message
68
+ * identified by nearOperand, and:
69
+ * - nearOperand refers to a message that wasn't moved outside the
70
+ * narrow specified by the link, or
71
+ * - nearOperand *might* refer to a message that was moved, but we don't
72
+ * know; we've tried and failed to find out.
73
+ *
74
+ * Or, use this to insist on the traditional meaning of "near" before
75
+ * the message-move feature: take the narrow's stream/topic/etc.
76
+ * literally, and open to the message "nearest" the given ID (sent
77
+ * around the same time), even if the message with that ID isn't
78
+ * actually in the narrow [1].
79
+ *
80
+ * User docs on moving messages:
81
+ * https://zulip.com/help/move-content-to-another-stream
82
+ * https://zulip.com/help/move-content-to-another-topic
83
+ *
84
+ * [1] Tim points out, at
85
+ * https://chat.zulip.org/#narrow/stream/101-design/topic/redirects.20from.20near.20links/near/1343095 :
86
+ * "[…] useful for situations where you might replace an existing
87
+ * search for `stream: 1/topic: 1/near: 15` with
88
+ * `stream: 2/topic: 2/near: 15` in order to view what was happening
89
+ * in another conversation at the same time as an existing
90
+ * conversation."
91
+ */
92
+ const noMove = ( ) => {
93
+ dispatch ( doNarrow ( narrow , nearOperand ) ) ;
94
+ } ;
95
+
96
+ const streamIdOperand =
97
+ isStreamNarrow ( narrow ) || isTopicNarrow ( narrow ) ? streamIdOfNarrow ( narrow ) : null ;
98
+ const topicOperand = isTopicNarrow ( narrow ) ? topicOfNarrow ( narrow ) : null ;
99
+
100
+ if ( streamIdOperand === null && topicOperand === null ) {
101
+ // Message moves only happen by changing the stream and/or topic.
102
+ noMove ( ) ;
103
+ return ;
104
+ }
105
+
106
+ // Grab the message and see if it was moved, so we can follow the move
107
+ // if so.
108
+
109
+ // Try to get it from our local data to avoid a server round-trip…
110
+ let message = messages . get ( nearOperand ) ;
111
+
112
+ // …but if we have to, go and ask the server.
113
+ // TODO: Give feedback when the server round trip takes longer than
114
+ // expected.
115
+ // TODO: Let the user cancel the request so we don't force a doNarrow
116
+ // after they've given up on tapping the link, and perhaps forgotten
117
+ // about it. Like any request, this might take well over a minute to
118
+ // resolve, or never resolve.
119
+ // TODO: When these are fixed, remove warning in jsdoc.
120
+ if ( ! message ) {
121
+ // TODO(server-5.0): Simplify.
122
+ if ( zulipFeatureLevel < 120 ) {
123
+ // api.getSingleMessage won't give us the message's stream and
124
+ // topic; see there. Hopefully the message wasn't moved.
125
+ noMove ( ) ;
126
+ return ;
127
+ }
128
+ try {
129
+ message = await api . getSingleMessage (
130
+ auth ,
131
+ { message_id : nearOperand } ,
132
+ zulipFeatureLevel ,
133
+ allowEditHistory ,
134
+ ) ;
135
+ } catch {
136
+ // Hopefully the message, if it exists or ever existed, wasn't moved.
137
+ noMove ( ) ;
138
+ return ;
139
+ }
140
+ }
141
+
142
+ // The FL 120 condition on calling api.getSingleMessage should ensure
143
+ // `message` isn't void.
144
+ // TODO(server-5.0): Simplify away.
145
+ if ( ! message ) {
146
+ logging . error ( '`message` from api.getSingleMessage unexpectedly falsy' ) ;
147
+ noMove ( ) ;
148
+ return ;
149
+ }
150
+
151
+ if ( message . type === 'private' ) {
152
+ // A PM could never have been moved.
153
+ noMove ( ) ;
154
+ return ;
155
+ }
156
+
157
+ if (
158
+ ( topicOperand === null || topicOperand === message . subject )
159
+ && ( streamIdOperand === null || streamIdOperand === message . stream_id )
160
+ ) {
161
+ // The message is still in the stream and/or topic in the link.
162
+ noMove ( ) ;
163
+ return ;
164
+ }
165
+
166
+ if (
167
+ ( topicOperand !== null && hasMessageEverHadTopic ( message , topicOperand ) === false )
168
+ || ( streamIdOperand !== null && hasMessageEverBeenInStream ( message , streamIdOperand ) === false )
169
+ ) {
170
+ // The message was never in the narrow specified by the link. That'd
171
+ // be an odd link to put in a message…anyway, perhaps we're meant to
172
+ // use the traditional meaning of "near"; see noMove's jsdoc
173
+ // for what that is.
174
+ noMove ( ) ;
175
+ return ;
176
+ }
177
+ // If we couldn't access the edit history in the checks above, assume
178
+ // the message was moved. It's the likeliest explanation why its topic
179
+ // and/or stream don't match the narrow link.
180
+
181
+ const { stream_id, subject } = message ;
182
+
183
+ // Reinterpret the link's narrow with the message's current stream
184
+ // and/or topic.
185
+ dispatch (
186
+ doNarrow (
187
+ caseNarrowDefault (
188
+ narrow ,
189
+ {
190
+ stream : ( ) => streamNarrow ( stream_id ) ,
191
+ topic : ( ) => topicNarrow ( stream_id , subject ) ,
192
+ } ,
193
+ ( ) => narrow ,
194
+ ) ,
195
+ nearOperand ,
196
+ ) ,
197
+ ) ;
198
+ } ;
199
+
24
200
export const messageLinkPress =
25
201
( href : string ) : ThunkAction < Promise < void >> =>
26
202
async ( dispatch , getState , { getGlobalSettings } ) => {
@@ -43,7 +219,7 @@ export const messageLinkPress =
43
219
return ;
44
220
}
45
221
46
- dispatch ( doNarrow ( narrow , nearOperand ) ) ;
222
+ await dispatch ( doNarrowNearLink ( narrow , nearOperand ) ) ;
47
223
} else if ( ! isUrlOnRealm ( href , auth . realm ) ) {
48
224
openLinkWithUserPreference ( href , getGlobalSettings ( ) ) ;
49
225
} else {
0 commit comments