[NEW] Add activity indicators for Uploading and Recording using new API; Support thread context; Deprecate the old typing API (#22392)

pull/23129/head
Sumukha Hegde 4 years ago committed by GitHub
parent 60b4759828
commit a024900596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      app/theme/client/imports/components/message-box.css
  2. 6
      app/theme/client/imports/general/variables.css
  3. 5
      app/ui-message/client/messageBox/messageBox.html
  4. 2
      app/ui-message/client/messageBox/messageBox.js
  5. 52
      app/ui-message/client/messageBox/messageBoxAudioMessage.js
  6. 20
      app/ui-message/client/messageBox/messageBoxTyping.html
  7. 35
      app/ui-message/client/messageBox/messageBoxTyping.js
  8. 14
      app/ui-message/client/messageBox/userActionIndicator.html
  9. 59
      app/ui-message/client/messageBox/userActionIndicator.ts
  10. 33
      app/ui-vrecord/client/vrecord.js
  11. 2
      app/ui/client/index.js
  12. 165
      app/ui/client/lib/UserAction.ts
  13. 10
      app/ui/client/lib/chatMessages.js
  14. 14
      app/ui/client/lib/fileUpload.js
  15. 109
      app/ui/client/lib/msgTyping.js
  16. 15
      definition/IUserAction.ts
  17. 6
      package-lock.json
  18. 2
      package.json
  19. 6
      packages/rocketchat-i18n/i18n/en.i18n.json
  20. 54
      server/modules/notifications/notifications.module.ts

@ -52,19 +52,19 @@
}
}
&__typing {
&__action {
position: absolute;
top: 4px;
left: 0;
margin-left: 24px;
color: var(--message-box-user-typing-color);
color: var(--message-box-user-activity-color);
font-size: var(--message-box-user-typing-text-size);
font-size: var(--message-box-user-activity-text-size);
&-user {
color: var(--message-box-user-typing-user-color);
color: var(--message-box-user-activity-user-color);
font-weight: bold;
}
@ -285,7 +285,7 @@
margin-top: 1rem;
padding: 0;
&__typing {
&__activity {
top: -1rem;
margin-left: 1rem;
@ -377,7 +377,7 @@
}
}
.rtl .rc-message-box__typing {
.rtl .rc-message-box__activity {
right: 0;
margin-right: 24px;

@ -335,9 +335,9 @@
--message-box-placeholder-color: var(--color-gray-medium);
--message-box-markdown-color: var(--color-gray);
--message-box-markdown-hover-color: var(--color-dark);
--message-box-user-typing-color: var(--color-gray);
--message-box-user-typing-text-size: 0.75rem;
--message-box-user-typing-user-color: var(--color-dark);
--message-box-user-activity-color: var(--color-gray);
--message-box-user-activity-text-size: 0.75rem;
--message-box-user-activity-user-color: var(--color-dark);
--message-box-container-border-color: var(--color-gray-medium);
--message-box-container-border-width: var(--border);
--message-box-container-border-radius: var(--border-radius);

@ -1,9 +1,8 @@
<template name="messageBox">
<div class="rc-message-box rc-new {{#if isEmbedded}}rc-message-box--embedded{{/if}}">
{{#unless isAnonymousOrMustJoinWithCode}}
{{#unless tmid}}
{{> messageBoxTyping rid=rid}}
{{/unless}}
{{> userActionIndicator rid=rid tmid=tmid}}
{{#if isWritable}}
{{#if popupConfig}}

@ -30,7 +30,7 @@ import {
} from '../../../utils/client';
import './messageBoxActions';
import './messageBoxReplyPreview';
import './messageBoxTyping';
import './userActionIndicator.ts';
import './messageBoxAudioMessage';
import './messageBoxNotSubscribed';
import './messageBox.html';

@ -2,31 +2,47 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { settings } from '../../../settings';
import { AudioRecorder, fileUpload } from '../../../ui';
import { AudioRecorder, fileUpload, USER_ACTIVITIES, UserAction } from '../../../ui';
import { t } from '../../../utils';
import './messageBoxAudioMessage.html';
const startRecording = () => new Promise((resolve, reject) =>
AudioRecorder.start((result) => (result ? resolve() : reject())));
const startRecording = async (rid, tmid) => {
try {
await AudioRecorder.start();
UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid });
} catch (error) {
throw error;
}
};
const stopRecording = () => new Promise((resolve) => AudioRecorder.stop(resolve));
const stopRecording = async (rid, tmid) => {
const result = await new Promise((resolve) => AudioRecorder.stop(resolve));
UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid });
return result;
};
const recordingInterval = new ReactiveVar(null);
const recordingRoomId = new ReactiveVar(null);
const cancelRecording = (instance) => new Promise(async () => {
const clearIntervalVariables = () => {
if (recordingInterval.get()) {
clearInterval(recordingInterval.get());
recordingInterval.set(null);
recordingRoomId.set(null);
}
};
const cancelRecording = async (instance, rid, tmid) => {
clearIntervalVariables();
instance.time.set('00:00');
await stopRecording();
const blob = await stopRecording(rid, tmid);
instance.state.set(null);
});
return blob;
};
Template.messageBoxAudioMessage.onCreated(async function() {
this.state = new ReactiveVar(null);
@ -63,7 +79,8 @@ Template.messageBoxAudioMessage.onCreated(async function() {
Template.messageBoxAudioMessage.onDestroyed(async function() {
if (this.state.get() === 'recording') {
await cancelRecording(this);
const { rid, tmid } = this.data;
await cancelRecording(this, rid, tmid);
}
});
@ -104,8 +121,7 @@ Template.messageBoxAudioMessage.events({
instance.state.set('recording');
try {
await startRecording();
await startRecording(this.rid, this.tmid);
const startTime = new Date();
recordingInterval.set(setInterval(() => {
const now = new Date();
@ -125,7 +141,7 @@ Template.messageBoxAudioMessage.events({
async 'click .js-audio-message-cancel'(event, instance) {
event.preventDefault();
await cancelRecording(instance);
await cancelRecording(instance, this.rid, this.tmid);
},
async 'click .js-audio-message-done'(event, instance) {
@ -133,19 +149,9 @@ Template.messageBoxAudioMessage.events({
instance.state.set('loading');
if (recordingInterval.get()) {
clearInterval(recordingInterval.get());
recordingInterval.set(null);
recordingRoomId.set(null);
}
instance.time.set('00:00');
const blob = await stopRecording();
instance.state.set(null);
const { rid, tmid } = this;
const blob = await cancelRecording(instance, rid, tmid);
await fileUpload([{ file: blob, type: 'video', name: `${ t('Audio record') }.mp3` }], { input: blob }, { rid, tmid });
},
});

@ -1,20 +0,0 @@
<template name="messageBoxTyping" args="rid">
{{#with data}}
<div class="rc-message-box__typing">
<span class="rc-message-box__typing-user">{{users}}</span>
{{#if multi}}
{{#if selfTyping}}
{{_ "are_also_typing"}}
{{else}}
{{_ "are_typing"}}
{{/if}}
{{else}}
{{#if selfTyping}}
{{_ "is_also_typing" context="male"}}
{{else}}
{{_ "is_typing" context="male"}}
{{/if}}
{{/if}}
</div>
{{/with}}
</template>

@ -1,35 +0,0 @@
import { Template } from 'meteor/templating';
import { MsgTyping } from '../../../ui';
import { t } from '../../../utils';
import { getConfig } from '../../../../client/lib/utils/getConfig';
import './messageBoxTyping.html';
const maxUsernames = parseInt(getConfig('max-usernames-typing')) || 4;
Template.messageBoxTyping.helpers({
data() {
const users = MsgTyping.get(this.rid);
if (users.length === 0) {
return;
}
if (users.length === 1) {
return {
multi: false,
selfTyping: MsgTyping.selfTyping,
users: users[0],
};
}
let last = users.pop();
if (users.length >= maxUsernames) {
last = t('others');
}
let usernames = users.slice(0, maxUsernames - 1).join(', ');
usernames = [usernames, last];
return {
multi: true,
selfTyping: MsgTyping.selfTyping,
users: usernames.join(` ${ t('and') } `),
};
},
});

@ -0,0 +1,14 @@
<template name="userActionIndicator" args="rid, tmid">
{{#if data}}
<div class="rc-message-box__action">
{{#each data}}
<span class="rc-message-box__action-user">{{ users }}</span>
{{#if multi}}
{{_ "are" }}&nbsp;{{_ action}}{{#unless end}},{{/unless}}
{{else}}
{{_ "is" }}&nbsp;{{_ action}}{{#unless end}},{{/unless}}
{{/if}}
{{/each}}
</div>
{{/if}}
</template>

@ -0,0 +1,59 @@
import { Template } from 'meteor/templating';
import { UserAction } from '../../../ui';
import { t } from '../../../utils/client';
import { getConfig } from '../../../../client/lib/utils/getConfig';
import './userActionIndicator.html';
const maxUsernames = parseInt(getConfig('max-usernames-typing') || '2');
Template.userActionIndicator.helpers({
data() {
const roomAction = UserAction.get(this.tmid || this.rid) || {};
if (!Object.keys(roomAction).length) {
return [];
}
const activities = Object.entries(roomAction);
const userActions = activities.map(([key, _users]) => {
const users = Object.keys(_users);
if (users.length === 0) {
return {
end: false,
};
}
const action = key.split('-')[1];
if (users.length === 1) {
return {
action,
multi: false,
users: users[0],
end: false,
};
}
let last = users.pop();
if (users.length >= maxUsernames) {
last = t('others');
}
const usernames = [users.slice(0, maxUsernames - 1).join(', '), last];
return {
action,
multi: true,
users: usernames.join(` ${ t('and') } `),
end: false,
};
}).filter((i) => i.action);
if (!Object.keys(userActions).length) {
return [];
}
// insert end=true for the last item.
userActions[userActions.length - 1].end = true;
return userActions;
},
});

@ -4,7 +4,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import _ from 'underscore';
import { VRecDialog } from './VRecDialog';
import { VideoRecorder, fileUpload } from '../../ui';
import { VideoRecorder, fileUpload, UserAction, USER_ACTIVITIES } from '../../ui';
Template.vrecDialog.helpers({
recordIcon() {
@ -33,26 +33,35 @@ Template.vrecDialog.helpers({
const recordingInterval = new ReactiveVar(null);
const stopVideoRecording = (rid, tmid) => {
if (recordingInterval.get()) {
clearInterval(recordingInterval.get());
recordingInterval.set(null);
}
UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid });
};
Template.vrecDialog.events({
'click .vrec-dialog .cancel'(e, t) {
const rid = t.rid.get();
const tmid = t.tmid.get();
VideoRecorder.stop();
VRecDialog.close();
t.time.set('');
if (recordingInterval.get()) {
clearInterval(recordingInterval.get());
recordingInterval.set(null);
}
stopVideoRecording(rid, tmid);
},
'click .vrec-dialog .record'(e, t) {
const rid = t.rid.get();
const tmid = t.tmid.get();
if (VideoRecorder.recording.get()) {
VideoRecorder.stopRecording();
if (recordingInterval.get()) {
clearInterval(recordingInterval.get());
recordingInterval.set(null);
}
stopVideoRecording(rid, tmid);
} else {
VideoRecorder.record();
UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid });
t.time.set('00:00');
const startTime = new Date();
recordingInterval.set(setInterval(() => {
@ -73,10 +82,7 @@ Template.vrecDialog.events({
};
VideoRecorder.stop(cb);
instance.time.set('');
if (recordingInterval.get()) {
clearInterval(recordingInterval.get());
recordingInterval.set(null);
}
stopVideoRecording(rid, tmid);
},
});
@ -124,5 +130,6 @@ Template.vrecDialog.onCreated(function() {
});
Template.vrecDialog.onDestroyed(function() {
VRecDialog.close(this.rid.get());
$(window).off('resize', this.remove);
});

@ -47,7 +47,7 @@ import './lib/Tooltip';
export { ChatMessages } from './lib/chatMessages';
export { fileUpload } from './lib/fileUpload';
export { MsgTyping } from './lib/msgTyping';
export { UserAction, USER_ACTIVITIES } from './lib/UserAction';
export { KonchatNotification } from './lib/notification';
export { Login, Button } from './lib/rocket';
export { AudioRecorder } from './lib/recorderjs/audioRecorder';

@ -0,0 +1,165 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Session } from 'meteor/session';
import { debounce } from 'lodash';
import { settings } from '../../../settings/client';
import { Notifications } from '../../../notifications/client';
import { IExtras, IRoomActivity, IActionsObject, IUser } from '../../../../definition/IUserAction';
const TIMEOUT = 15000;
const RENEW = TIMEOUT / 3;
export const USER_ACTIVITY = 'user-activity';
export const USER_ACTIVITIES = {
USER_RECORDING: 'user-recording',
USER_TYPING: 'user-typing',
USER_UPLOADING: 'user-uploading',
};
const activityTimeouts = new Map();
const activityRenews = new Map();
const continuingIntervals = new Map();
const roomActivities = new Map<string, Set<string>>();
const rooms = new Map<string, Function>();
const performingUsers = new ReactiveDict<IActionsObject>();
const shownName = function(user: IUser | null | undefined): string|undefined {
if (!user) {
return;
}
if (settings.get('UI_Use_Real_Name')) {
return user.name;
}
return user.username;
};
const emitActivities = debounce((rid: string, extras: IExtras): void => {
const activities = roomActivities.get(extras?.tmid || rid) || new Set();
Notifications.notifyRoom(rid, USER_ACTIVITY, shownName(Meteor.user()), [...activities], extras);
}, 500);
function handleStreamAction(rid: string, username: string, activityTypes: string[], extras?: IExtras): void {
rid = extras?.tmid || rid;
const roomActivities = performingUsers.get(rid) || {};
for (const [, activity] of Object.entries(USER_ACTIVITIES)) {
roomActivities[activity] = roomActivities[activity] || new Map();
const users = roomActivities[activity];
const timeout = users[username];
if (timeout) {
clearTimeout(timeout);
}
if (activityTypes.includes(activity)) {
activityTypes.splice(activityTypes.indexOf(activity), 1);
users[username] = setTimeout(() => handleStreamAction(rid, username, activityTypes, extras), TIMEOUT);
} else {
delete users[username];
}
}
performingUsers.set(rid, roomActivities);
}
export const UserAction = new class {
constructor() {
Tracker.autorun(() => Session.get('openedRoom') && this.addStream(Session.get('openedRoom')));
}
addStream(rid: string): void {
if (rooms.get(rid)) {
return;
}
const handler = function(username: string, activityType: string[], extras?: object): void {
const user = Meteor.users.findOne(Meteor.userId() || undefined, { fields: { name: 1, username: 1 } });
if (username === shownName(user)) {
return;
}
handleStreamAction(rid, username, activityType, extras);
};
rooms.set(rid, handler);
Notifications.onRoom(rid, USER_ACTIVITY, handler);
}
performContinuously(rid: string, activityType: string, extras: IExtras = {}): void {
const trid = extras?.tmid || rid;
const key = `${ activityType }-${ trid }`;
if (continuingIntervals.get(key)) {
return;
}
this.start(rid, activityType, extras);
continuingIntervals.set(key, setInterval(() => {
this.start(rid, activityType, extras);
}, RENEW));
}
start(rid: string, activityType: string, extras: IExtras = {}): void {
const trid = extras?.tmid || rid;
const key = `${ activityType }-${ trid }`;
if (activityRenews.get(key)) {
return;
}
activityRenews.set(key, setTimeout(() => {
clearTimeout(activityRenews.get(key));
activityRenews.delete(key);
}, RENEW));
const activities = roomActivities.get(trid) || new Set();
activities.add(activityType);
roomActivities.set(trid, activities);
emitActivities(rid, extras);
if (activityTimeouts.get(key)) {
clearTimeout(activityTimeouts.get(key));
activityTimeouts.delete(key);
}
activityTimeouts.set(key, setTimeout(() => this.stop(trid, activityType, extras), TIMEOUT));
activityTimeouts.get(key);
}
stop(rid: string, activityType: string, extras: IExtras): void {
const trid = extras?.tmid || rid;
const key = `${ activityType }-${ trid }`;
if (activityTimeouts.get(key)) {
clearTimeout(activityTimeouts.get(key));
activityTimeouts.delete(key);
}
if (activityRenews.get(key)) {
clearTimeout(activityRenews.get(key));
activityRenews.delete(key);
}
if (continuingIntervals.get(key)) {
clearInterval(continuingIntervals.get(key));
continuingIntervals.delete(key);
}
const activities = roomActivities.get(trid) || new Set();
activities.delete(activityType);
roomActivities.set(trid, activities);
emitActivities(rid, extras);
}
cancel(rid: string): void {
if (!rooms.get(rid)) {
return;
}
Notifications.unRoom(rid, USER_ACTIVITY, rooms.get(rid));
rooms.delete(rid);
}
get(roomId: string): IRoomActivity | undefined {
return performingUsers.get(roomId);
}
}();

@ -9,7 +9,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { escapeHTML } from '@rocket.chat/string-helpers';
import { KonchatNotification } from './notification';
import { MsgTyping } from './msgTyping';
import { UserAction, USER_ACTIVITIES } from '../index';
import { fileUpload } from './fileUpload';
import { t, slashCommands } from '../../../utils/client';
import {
@ -255,7 +255,7 @@ export class ChatMessages {
async send(event, { rid, tmid, value, tshow }, done = () => {}) {
const threadsEnabled = settings.get('Threads_enabled');
MsgTyping.stop(rid);
UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
if (!ChatSubscription.findOne({ rid })) {
await callWithErrorHandling('joinRoom', rid);
@ -580,9 +580,9 @@ export class ChatMessages {
if (!Object.values(keyCodes).includes(keyCode)) {
if (input.value.trim()) {
MsgTyping.start(rid);
UserAction.start(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
} else {
MsgTyping.stop(rid);
UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
}
}
@ -590,6 +590,6 @@ export class ChatMessages {
}
onDestroyed(rid) {
MsgTyping.cancel(rid);
UserAction.cancel(rid);
}
}

@ -3,6 +3,7 @@ import { Session } from 'meteor/session';
import { Random } from 'meteor/random';
import { settings } from '../../../settings/client';
import { UserAction, USER_ACTIVITIES } from '../index';
import { fileUploadIsValidContentType, APIClient } from '../../../utils';
import { imperativeModal } from '../../../../client/lib/imperativeModal';
import FileUploadModal from '../../../../client/components/modals/FileUploadModal';
@ -47,6 +48,9 @@ export const uploadFileWithMessage = async (rid, tmid, { description, fileName,
Session.set('uploading', uploads);
},
});
if (Session.get('uploading').length) {
UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid });
}
Tracker.autorun((computation) => {
const isCanceling = Session.get(`uploading-cancel-${ upload.id }`);
@ -65,13 +69,21 @@ export const uploadFileWithMessage = async (rid, tmid, { description, fileName,
try {
await promise;
const uploads = Session.get('uploading') || [];
return Session.set('uploading', uploads.filter((u) => u.id !== upload.id));
const remainingUploads = Session.set('uploading', uploads.filter((u) => u.id !== upload.id));
if (!Session.get('uploading').length) {
UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid });
}
return remainingUploads;
} catch (error) {
const uploads = Session.get('uploading') || [];
uploads.filter((u) => u.id === upload.id).forEach((u) => {
u.error = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message;
u.percentage = 0;
});
if (!uploads.length) {
UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid });
}
Session.set('uploading', uploads);
}
};

@ -1,109 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Session } from 'meteor/session';
import _ from 'underscore';
import { settings } from '../../../settings';
import { Notifications } from '../../../notifications';
const shownName = function(user) {
if (!user) {
return;
}
if (settings.get('UI_Use_Real_Name')) {
return user.name;
}
return user.username;
};
const timeouts = {};
const timeout = 15000;
const renew = timeout / 3;
const renews = {};
const rooms = {};
const selfTyping = new ReactiveVar(false);
const usersTyping = new ReactiveDict();
const stopTyping = (rid) => Notifications.notifyRoom(rid, 'typing', shownName(Meteor.user()), false);
const typing = (rid) => Notifications.notifyRoom(rid, 'typing', shownName(Meteor.user()), true);
export const MsgTyping = new class {
constructor() {
Tracker.autorun(() => Session.get('openedRoom') && this.addStream(Session.get('openedRoom')));
}
get selfTyping() { return selfTyping.get(); }
cancel(rid) {
if (rooms[rid]) {
Notifications.unRoom(rid, 'typing', rooms[rid]);
Object.values(usersTyping.get(rid) || {}).forEach(clearTimeout);
usersTyping.set(rid);
delete rooms[rid];
}
}
addStream(rid) {
if (rooms[rid]) {
return;
}
rooms[rid] = function(username, typing) {
const user = Meteor.users.findOne(Meteor.userId(), { fields: { name: 1, username: 1 } });
if (username === shownName(user)) {
return;
}
const users = usersTyping.get(rid) || {};
if (typing === true) {
clearTimeout(users[username]);
users[username] = setTimeout(function() {
const u = usersTyping.get(rid);
delete u[username];
usersTyping.set(rid, u);
}, timeout);
} else {
delete users[username];
}
usersTyping.set(rid, users);
};
return Notifications.onRoom(rid, 'typing', rooms[rid]);
}
stop(rid) {
selfTyping.set(false);
if (timeouts[rid]) {
clearTimeout(timeouts[rid]);
delete timeouts[rid];
delete renews[rid];
}
return stopTyping(rid);
}
start(rid) {
selfTyping.set(true);
if (renews[rid]) {
return;
}
renews[rid] = setTimeout(() => delete renews[rid], renew);
typing(rid);
if (timeouts[rid]) {
clearTimeout(timeouts[rid]);
}
timeouts[rid] = setTimeout(() => this.stop(rid), timeout);
return timeouts[rid];
}
get(rid) {
return _.keys(usersTyping.get(rid)) || [];
}
}();

@ -0,0 +1,15 @@
export type IUser = {
_id: string;
username?: string;
name?: string;
}
export type IExtras = {
tmid?: string;
}
export type IActivity = Record<string, NodeJS.Timeout>
export type IRoomActivity = Record<string, IActivity>
export type IActionsObject = Record<string, IRoomActivity>;

6
package-lock.json generated

@ -11226,9 +11226,9 @@
}
},
"@types/meteor": {
"version": "1.4.71",
"resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.71.tgz",
"integrity": "sha512-/ytEdaVCQsYLf4eh6k1muW2vQK49w2cyCnnGj0ZTuTLr8L5XvqMchQ1kA8aUmB5or8diote14ZaQCG4mycQziQ==",
"version": "1.4.74",
"resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.74.tgz",
"integrity": "sha512-x/JDB50WaRhLKvqDo5r47yHjljMQ3bJxuPnhp/I/1NR/0+CQZcXJvkv7HXsJs4csN95JTo1Xp7du6/P7M8pN3A==",
"dev": true,
"requires": {
"@types/connect": "*",

@ -82,7 +82,7 @@
"@types/lodash.get": "^4.4.6",
"@types/mailparser": "^3.0.2",
"@types/marked": "^1.2.2",
"@types/meteor": "^1.4.71",
"@types/meteor": "1.4.74",
"@types/mkdirp": "^1.0.1",
"@types/mocha": "^8.2.2",
"@types/mock-require": "^2.0.0",

@ -528,7 +528,6 @@
"Archive": "Archive",
"archive-room": "Archive Room",
"archive-room_description": "Permission to archive a channel",
"are_also_typing": "are also typing",
"are_typing": "are typing",
"Are_you_sure": "Are you sure?",
"Are_you_sure_you_want_to_clear_all_unread_messages": "Are you sure you want to clear all unread messages?",
@ -2309,9 +2308,6 @@
"IRC_Port": "The port to bind to on the IRC host server.",
"IRC_Private_Message": "Output of the PRIVMSG command.",
"IRC_Quit": "Output upon quitting an IRC session.",
"is_also_typing": "is also typing",
"is_also_typing_female": "is also typing",
"is_also_typing_male": "is also typing",
"is_typing": "is typing",
"is_typing_female": "is typing",
"is_typing_male": "is typing",
@ -3402,6 +3398,7 @@
"Receive_Group_Mentions": "Receive @all and @here mentions",
"Recent_Import_History": "Recent Import History",
"Record": "Record",
"recording": "recording",
"Redirect_URI": "Redirect URI",
"Refresh": "Refresh",
"Refresh_keys": "Refresh keys",
@ -4258,6 +4255,7 @@
"Two-factor_authentication_is_currently_disabled": "Two-factor authentication via TOTP is currently disabled",
"Two-factor_authentication_native_mobile_app_warning": "WARNING: Once you enable this, you will not be able to login on the native mobile apps (Rocket.Chat+) using your password until they implement the 2FA.",
"Type": "Type",
"typing": "typing",
"Types": "Types",
"Types_and_Distribution": "Types and Distribution",
"Type_your_email": "Type your email",

@ -179,22 +179,8 @@ export class NotificationsModule {
return subsCount > 0;
});
this.streamRoom.allowWrite(async function(eventName, username, _typing, extraData): Promise<boolean> {
const [rid, e] = eventName.split('/');
// TODO should this use WEB_RTC_EVENTS enum?
if (e === 'webrtc') {
return true;
}
if (e !== 'typing') {
return false;
}
async function canType({ userId, username, extraData, rid }: {userId?: string; username: string; extraData?: {token: string}; rid: string}): Promise<boolean> {
try {
// TODO consider using something to cache settings
const key = await Settings.getValueById('UI_Use_Real_Name') ? 'name' : 'username';
// typing from livechat widget
if (extraData?.token) {
// TODO improve this to make a query 'v.token'
@ -202,15 +188,19 @@ export class NotificationsModule {
return !!room && room.t === 'l' && room.v.token === extraData.token;
}
if (!this.userId) {
if (!userId) {
return false;
}
const user = await Users.findOneById<Pick<IUser, 'name' | 'username'>>(this.userId, {
// TODO consider using something to cache settings
const key = await Settings.getValueById('UI_Use_Real_Name') ? 'name' : 'username';
const user = await Users.findOneById<Pick<IUser, 'name' | 'username'>>(userId, {
projection: {
[key]: 1,
},
});
if (!user) {
return false;
}
@ -220,6 +210,36 @@ export class NotificationsModule {
SystemLogger.error(e);
return false;
}
}
const { streamRoom } = this;
this.streamRoom.allowWrite(async function(eventName, username, _activity, extraData): Promise<boolean> {
const [rid, e] = eventName.split('/');
// TODO should this use WEB_RTC_EVENTS enum?
if (e === 'webrtc') {
return true;
}
// In fact user-activity streamer will handle typing action.
// Need to use 'typing' streamer till all other clients updated to use user-activity streamer.
if (e !== 'typing' && e !== 'user-activity') {
return false;
}
if (!await canType({ extraData, rid, username, userId: this.userId })) {
return false;
}
// DEPRECATED
// Keep compatibility between old and new events
if (e === 'user-activity' && Array.isArray(_activity) && (_activity.length === 0 || _activity.includes('user-typing'))) {
streamRoom.emit(`${ rid }/typing`, username, _activity.includes('user-typing'));
} else if (e === 'typing') {
streamRoom.emit(`${ rid }/user-activity`, username, _activity ? ['user-typing'] : [], extraData);
}
return true;
});
this.streamRoomUsers.allowRead('none');

Loading…
Cancel
Save