1 | ;
|
2 |
|
3 | var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
|
4 |
|
5 | Object.defineProperty(exports, "__esModule", {
|
6 | value: true
|
7 | });
|
8 | exports.RoomMember = RoomMember;
|
9 |
|
10 | var _events = require("events");
|
11 |
|
12 | var _contentRepo = require("../content-repo");
|
13 |
|
14 | var utils = _interopRequireWildcard(require("../utils"));
|
15 |
|
16 | /*
|
17 | Copyright 2015, 2016 OpenMarket Ltd
|
18 | Copyright 2019 The Matrix.org Foundation C.I.C.
|
19 |
|
20 | Licensed under the Apache License, Version 2.0 (the "License");
|
21 | you may not use this file except in compliance with the License.
|
22 | You may obtain a copy of the License at
|
23 |
|
24 | http://www.apache.org/licenses/LICENSE-2.0
|
25 |
|
26 | Unless required by applicable law or agreed to in writing, software
|
27 | distributed under the License is distributed on an "AS IS" BASIS,
|
28 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
29 | See the License for the specific language governing permissions and
|
30 | limitations under the License.
|
31 | */
|
32 |
|
33 | /**
|
34 | * @module models/room-member
|
35 | */
|
36 |
|
37 | /**
|
38 | * Construct a new room member.
|
39 | *
|
40 | * @constructor
|
41 | * @alias module:models/room-member
|
42 | *
|
43 | * @param {string} roomId The room ID of the member.
|
44 | * @param {string} userId The user ID of the member.
|
45 | * @prop {string} roomId The room ID for this member.
|
46 | * @prop {string} userId The user ID of this member.
|
47 | * @prop {boolean} typing True if the room member is currently typing.
|
48 | * @prop {string} name The human-readable name for this room member. This will be
|
49 | * disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the
|
50 | * same displayname.
|
51 | * @prop {string} rawDisplayName The ambiguous displayname of this room member.
|
52 | * @prop {Number} powerLevel The power level for this room member.
|
53 | * @prop {Number} powerLevelNorm The normalised power level (0-100) for this
|
54 | * room member.
|
55 | * @prop {User} user The User object for this room member, if one exists.
|
56 | * @prop {string} membership The membership state for this room member e.g. 'join'.
|
57 | * @prop {Object} events The events describing this RoomMember.
|
58 | * @prop {MatrixEvent} events.member The m.room.member event for this RoomMember.
|
59 | */
|
60 | function RoomMember(roomId, userId) {
|
61 | this.roomId = roomId;
|
62 | this.userId = userId;
|
63 | this.typing = false;
|
64 | this.name = userId;
|
65 | this.rawDisplayName = userId;
|
66 | this.powerLevel = 0;
|
67 | this.powerLevelNorm = 0;
|
68 | this.user = null;
|
69 | this.membership = null;
|
70 | this.events = {
|
71 | member: null
|
72 | };
|
73 | this._isOutOfBand = false;
|
74 |
|
75 | this._updateModifiedTime();
|
76 | }
|
77 |
|
78 | utils.inherits(RoomMember, _events.EventEmitter);
|
79 | /**
|
80 | * Mark the member as coming from a channel that is not sync
|
81 | */
|
82 |
|
83 | RoomMember.prototype.markOutOfBand = function () {
|
84 | this._isOutOfBand = true;
|
85 | };
|
86 | /**
|
87 | * @return {bool} does the member come from a channel that is not sync?
|
88 | * This is used to store the member seperately
|
89 | * from the sync state so it available across browser sessions.
|
90 | */
|
91 |
|
92 |
|
93 | RoomMember.prototype.isOutOfBand = function () {
|
94 | return this._isOutOfBand;
|
95 | };
|
96 | /**
|
97 | * Update this room member's membership event. May fire "RoomMember.name" if
|
98 | * this event updates this member's name.
|
99 | * @param {MatrixEvent} event The <code>m.room.member</code> event
|
100 | * @param {RoomState} roomState Optional. The room state to take into account
|
101 | * when calculating (e.g. for disambiguating users with the same name).
|
102 | * @fires module:client~MatrixClient#event:"RoomMember.name"
|
103 | * @fires module:client~MatrixClient#event:"RoomMember.membership"
|
104 | */
|
105 |
|
106 |
|
107 | RoomMember.prototype.setMembershipEvent = function (event, roomState) {
|
108 | if (event.getType() !== "m.room.member") {
|
109 | return;
|
110 | }
|
111 |
|
112 | this._isOutOfBand = false;
|
113 | this.events.member = event;
|
114 | const oldMembership = this.membership;
|
115 | this.membership = event.getDirectionalContent().membership;
|
116 | const oldName = this.name;
|
117 | this.name = calculateDisplayName(this.userId, event.getDirectionalContent().displayname, roomState);
|
118 | this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
|
119 |
|
120 | if (oldMembership !== this.membership) {
|
121 | this._updateModifiedTime();
|
122 |
|
123 | this.emit("RoomMember.membership", event, this, oldMembership);
|
124 | }
|
125 |
|
126 | if (oldName !== this.name) {
|
127 | this._updateModifiedTime();
|
128 |
|
129 | this.emit("RoomMember.name", event, this, oldName);
|
130 | }
|
131 | };
|
132 | /**
|
133 | * Update this room member's power level event. May fire
|
134 | * "RoomMember.powerLevel" if this event updates this member's power levels.
|
135 | * @param {MatrixEvent} powerLevelEvent The <code>m.room.power_levels</code>
|
136 | * event
|
137 | * @fires module:client~MatrixClient#event:"RoomMember.powerLevel"
|
138 | */
|
139 |
|
140 |
|
141 | RoomMember.prototype.setPowerLevelEvent = function (powerLevelEvent) {
|
142 | if (powerLevelEvent.getType() !== "m.room.power_levels") {
|
143 | return;
|
144 | }
|
145 |
|
146 | const evContent = powerLevelEvent.getDirectionalContent();
|
147 | let maxLevel = evContent.users_default || 0;
|
148 | utils.forEach(utils.values(evContent.users), function (lvl) {
|
149 | maxLevel = Math.max(maxLevel, lvl);
|
150 | });
|
151 | const oldPowerLevel = this.powerLevel;
|
152 | const oldPowerLevelNorm = this.powerLevelNorm;
|
153 |
|
154 | if (evContent.users && evContent.users[this.userId] !== undefined) {
|
155 | this.powerLevel = evContent.users[this.userId];
|
156 | } else if (evContent.users_default !== undefined) {
|
157 | this.powerLevel = evContent.users_default;
|
158 | } else {
|
159 | this.powerLevel = 0;
|
160 | }
|
161 |
|
162 | this.powerLevelNorm = 0;
|
163 |
|
164 | if (maxLevel > 0) {
|
165 | this.powerLevelNorm = this.powerLevel * 100 / maxLevel;
|
166 | } // emit for changes in powerLevelNorm as well (since the app will need to
|
167 | // redraw everyone's level if the max has changed)
|
168 |
|
169 |
|
170 | if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
171 | this._updateModifiedTime();
|
172 |
|
173 | this.emit("RoomMember.powerLevel", powerLevelEvent, this);
|
174 | }
|
175 | };
|
176 | /**
|
177 | * Update this room member's typing event. May fire "RoomMember.typing" if
|
178 | * this event changes this member's typing state.
|
179 | * @param {MatrixEvent} event The typing event
|
180 | * @fires module:client~MatrixClient#event:"RoomMember.typing"
|
181 | */
|
182 |
|
183 |
|
184 | RoomMember.prototype.setTypingEvent = function (event) {
|
185 | if (event.getType() !== "m.typing") {
|
186 | return;
|
187 | }
|
188 |
|
189 | const oldTyping = this.typing;
|
190 | this.typing = false;
|
191 | const typingList = event.getContent().user_ids;
|
192 |
|
193 | if (!utils.isArray(typingList)) {
|
194 | // malformed event :/ bail early. TODO: whine?
|
195 | return;
|
196 | }
|
197 |
|
198 | if (typingList.indexOf(this.userId) !== -1) {
|
199 | this.typing = true;
|
200 | }
|
201 |
|
202 | if (oldTyping !== this.typing) {
|
203 | this._updateModifiedTime();
|
204 |
|
205 | this.emit("RoomMember.typing", event, this);
|
206 | }
|
207 | };
|
208 | /**
|
209 | * Update the last modified time to the current time.
|
210 | */
|
211 |
|
212 |
|
213 | RoomMember.prototype._updateModifiedTime = function () {
|
214 | this._modified = Date.now();
|
215 | };
|
216 | /**
|
217 | * Get the timestamp when this RoomMember was last updated. This timestamp is
|
218 | * updated when properties on this RoomMember are updated.
|
219 | * It is updated <i>before</i> firing events.
|
220 | * @return {number} The timestamp
|
221 | */
|
222 |
|
223 |
|
224 | RoomMember.prototype.getLastModifiedTime = function () {
|
225 | return this._modified;
|
226 | };
|
227 |
|
228 | RoomMember.prototype.isKicked = function () {
|
229 | return this.membership === "leave" && this.events.member.getSender() !== this.events.member.getStateKey();
|
230 | };
|
231 | /**
|
232 | * If this member was invited with the is_direct flag set, return
|
233 | * the user that invited this member
|
234 | * @return {string} user id of the inviter
|
235 | */
|
236 |
|
237 |
|
238 | RoomMember.prototype.getDMInviter = function () {
|
239 | // when not available because that room state hasn't been loaded in,
|
240 | // we don't really know, but more likely to not be a direct chat
|
241 | if (this.events.member) {
|
242 | // TODO: persist the is_direct flag on the member as more member events
|
243 | // come in caused by displayName changes.
|
244 | // the is_direct flag is set on the invite member event.
|
245 | // This is copied on the prev_content section of the join member event
|
246 | // when the invite is accepted.
|
247 | const memberEvent = this.events.member;
|
248 | let memberContent = memberEvent.getContent();
|
249 | let inviteSender = memberEvent.getSender();
|
250 |
|
251 | if (memberContent.membership === "join") {
|
252 | memberContent = memberEvent.getPrevContent();
|
253 | inviteSender = memberEvent.getUnsigned().prev_sender;
|
254 | }
|
255 |
|
256 | if (memberContent.membership === "invite" && memberContent.is_direct) {
|
257 | return inviteSender;
|
258 | }
|
259 | }
|
260 | };
|
261 | /**
|
262 | * Get the avatar URL for a room member.
|
263 | * @param {string} baseUrl The base homeserver URL See
|
264 | * {@link module:client~MatrixClient#getHomeserverUrl}.
|
265 | * @param {Number} width The desired width of the thumbnail.
|
266 | * @param {Number} height The desired height of the thumbnail.
|
267 | * @param {string} resizeMethod The thumbnail resize method to use, either
|
268 | * "crop" or "scale".
|
269 | * @param {Boolean} allowDefault (optional) Passing false causes this method to
|
270 | * return null if the user has no avatar image. Otherwise, a default image URL
|
271 | * will be returned. Default: true. (Deprecated)
|
272 | * @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
|
273 | * returned even if it is a direct hyperlink rather than a matrix content URL.
|
274 | * If false, any non-matrix content URLs will be ignored. Setting this option to
|
275 | * true will expose URLs that, if fetched, will leak information about the user
|
276 | * to anyone who they share a room with.
|
277 | * @return {?string} the avatar URL or null.
|
278 | */
|
279 |
|
280 |
|
281 | RoomMember.prototype.getAvatarUrl = function (baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
|
282 | if (allowDefault === undefined) {
|
283 | allowDefault = true;
|
284 | }
|
285 |
|
286 | const rawUrl = this.getMxcAvatarUrl();
|
287 |
|
288 | if (!rawUrl && !allowDefault) {
|
289 | return null;
|
290 | }
|
291 |
|
292 | const httpUrl = (0, _contentRepo.getHttpUriForMxc)(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks);
|
293 |
|
294 | if (httpUrl) {
|
295 | return httpUrl;
|
296 | } else if (allowDefault) {
|
297 | return (0, _contentRepo.getIdenticonUri)(baseUrl, this.userId, width, height);
|
298 | }
|
299 |
|
300 | return null;
|
301 | };
|
302 | /**
|
303 | * get the mxc avatar url, either from a state event, or from a lazily loaded member
|
304 | * @return {string} the mxc avatar url
|
305 | */
|
306 |
|
307 |
|
308 | RoomMember.prototype.getMxcAvatarUrl = function () {
|
309 | if (this.events.member) {
|
310 | return this.events.member.getDirectionalContent().avatar_url;
|
311 | } else if (this.user) {
|
312 | return this.user.avatarUrl;
|
313 | }
|
314 |
|
315 | return null;
|
316 | };
|
317 |
|
318 | function calculateDisplayName(selfUserId, displayName, roomState) {
|
319 | if (!displayName || displayName === selfUserId) {
|
320 | return selfUserId;
|
321 | } // First check if the displayname is something we consider truthy
|
322 | // after stripping it of zero width characters and padding spaces
|
323 |
|
324 |
|
325 | if (!utils.removeHiddenChars(displayName)) {
|
326 | return selfUserId;
|
327 | }
|
328 |
|
329 | if (!roomState) {
|
330 | return displayName;
|
331 | } // Next check if the name contains something that look like a mxid
|
332 | // If it does, it may be someone trying to impersonate someone else
|
333 | // Show full mxid in this case
|
334 |
|
335 |
|
336 | let disambiguate = /@.+:.+/.test(displayName);
|
337 |
|
338 | if (!disambiguate) {
|
339 | // Also show mxid if the display name contains any LTR/RTL characters as these
|
340 | // make it very difficult for us to find similar *looking* display names
|
341 | // E.g "Mark" could be cloned by writing "kraM" but in RTL.
|
342 | disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName);
|
343 | }
|
344 |
|
345 | if (!disambiguate) {
|
346 | // Also show mxid if there are other people with the same or similar
|
347 | // displayname, after hidden character removal.
|
348 | const userIds = roomState.getUserIdsWithDisplayName(displayName);
|
349 | disambiguate = userIds.some(u => u !== selfUserId);
|
350 | }
|
351 |
|
352 | if (disambiguate) {
|
353 | return displayName + " (" + selfUserId + ")";
|
354 | }
|
355 |
|
356 | return displayName;
|
357 | }
|
358 | /**
|
359 | * Fires whenever any room member's name changes.
|
360 | * @event module:client~MatrixClient#"RoomMember.name"
|
361 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
362 | * @param {RoomMember} member The member whose RoomMember.name changed.
|
363 | * @param {string?} oldName The previous name. Null if the member didn't have a
|
364 | * name previously.
|
365 | * @example
|
366 | * matrixClient.on("RoomMember.name", function(event, member){
|
367 | * var newName = member.name;
|
368 | * });
|
369 | */
|
370 |
|
371 | /**
|
372 | * Fires whenever any room member's membership state changes.
|
373 | * @event module:client~MatrixClient#"RoomMember.membership"
|
374 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
375 | * @param {RoomMember} member The member whose RoomMember.membership changed.
|
376 | * @param {string?} oldMembership The previous membership state. Null if it's a
|
377 | * new member.
|
378 | * @example
|
379 | * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
|
380 | * var newState = member.membership;
|
381 | * });
|
382 | */
|
383 |
|
384 | /**
|
385 | * Fires whenever any room member's typing state changes.
|
386 | * @event module:client~MatrixClient#"RoomMember.typing"
|
387 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
388 | * @param {RoomMember} member The member whose RoomMember.typing changed.
|
389 | * @example
|
390 | * matrixClient.on("RoomMember.typing", function(event, member){
|
391 | * var isTyping = member.typing;
|
392 | * });
|
393 | */
|
394 |
|
395 | /**
|
396 | * Fires whenever any room member's power level changes.
|
397 | * @event module:client~MatrixClient#"RoomMember.powerLevel"
|
398 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
399 | * @param {RoomMember} member The member whose RoomMember.powerLevel changed.
|
400 | * @example
|
401 | * matrixClient.on("RoomMember.powerLevel", function(event, member){
|
402 | * var newPowerLevel = member.powerLevel;
|
403 | * var newNormPowerLevel = member.powerLevelNorm;
|
404 | * });
|
405 | */ |
\ | No newline at end of file |