Rename Threads to Discussion (#13782)

pull/13752/head^2
Guilherme Gazzo 6 years ago committed by Diego Sampaio
parent ceb653921b
commit be18e1898d
  1. 2
      app/channel-settings/server/methods/saveRoomSettings.js
  2. 53
      app/discussion/client/createDiscussionMessageAction.js
  3. 36
      app/discussion/client/discussionFromMessageBox.js
  4. 16
      app/discussion/client/index.js
  5. 3
      app/discussion/client/lib/discussionsOfRoom.js
  6. 17
      app/discussion/client/lib/messageTypes/discussionMessage.js
  7. 22
      app/discussion/client/public/stylesheets/discussion.css
  8. 11
      app/discussion/client/tabBar.js
  9. 4
      app/discussion/client/views/DiscussionList.html
  10. 12
      app/discussion/client/views/DiscussionList.js
  11. 8
      app/discussion/client/views/DiscussionTabbar.html
  12. 25
      app/discussion/client/views/DiscussionTabbar.js
  13. 63
      app/discussion/client/views/creationDialog/CreateDiscussion.html
  14. 68
      app/discussion/client/views/creationDialog/CreateDiscussion.js
  15. 0
      app/discussion/index.js
  16. 12
      app/discussion/lib/discussionRoomType.js
  17. 0
      app/discussion/server/authorization.js
  18. 38
      app/discussion/server/config.js
  19. 9
      app/discussion/server/hooks/joinDiscussionOnMessage.js
  20. 27
      app/discussion/server/hooks/propagateDiscussionMetadata.js
  21. 14
      app/discussion/server/index.js
  22. 133
      app/discussion/server/methods/createDiscussion.js
  23. 6
      app/discussion/server/permissions.js
  24. 8
      app/discussion/server/publications/discussionParentAutocomplete.js
  25. 32
      app/discussion/server/publications/discussionsOfRoom.js
  26. 2
      app/lib/lib/roomTypes/private.js
  27. 2
      app/lib/lib/roomTypes/public.js
  28. 2
      app/lib/server/functions/addUserToRoom.js
  29. 14
      app/lib/server/functions/cleanRoomHistory.js
  30. 4
      app/lib/server/methods/cleanRoomHistory.js
  31. 5
      app/message-attachments/client/messageAttachment.html
  32. 2
      app/message-attachments/client/stylesheets/messageAttachments.css
  33. 46
      app/models/server/models/Messages.js
  34. 6
      app/models/server/models/Rooms.js
  35. 6
      app/retention-policy/server/cronPruneMessages.js
  36. 2
      app/theme/client/imports/components/header.css
  37. 2
      app/theme/client/imports/components/modal.css
  38. 4
      app/theme/client/imports/components/modal/create-channel.css
  39. 2
      app/theme/client/imports/components/userInfo.css
  40. 7
      app/theme/client/imports/forms/button.css
  41. 10
      app/theme/client/imports/forms/input.css
  42. 8
      app/theme/client/imports/general/base_old.css
  43. 4
      app/theme/client/imports/general/variables.css
  44. 49
      app/threading/client/createThreadMessageAction.js
  45. 20
      app/threading/client/index.js
  46. 32
      app/threading/client/lib/messageTypes/threadMessage.js
  47. 3
      app/threading/client/lib/threadsOfRoom.js
  48. 34
      app/threading/client/public/stylesheets/threading.css
  49. 33
      app/threading/client/threadFromMessageBox.js
  50. 7
      app/threading/client/views/fieldTypeThreadLastMessageAge.html
  51. 12
      app/threading/client/views/fieldTypeThreadLastMessageAge.js
  52. 19
      app/threading/client/views/fieldTypeThreadReplyCounter.html
  53. 30
      app/threading/client/views/fieldTypeThreadReplyCounter.js
  54. 42
      app/threading/server/config.js
  55. 28
      app/threading/server/hooks/propagateThreadMetadata.js
  56. 14
      app/threading/server/index.js
  57. 162
      app/threading/server/methods/createThread.js
  58. 31
      app/threading/server/publications/threadsOfRoom.js
  59. 8
      app/ui-account/client/accountPreferences.html
  60. 2
      app/ui-account/client/accountPreferences.js
  61. 4
      app/ui-clean-history/client/views/cleanHistory.html
  62. 14
      app/ui-clean-history/client/views/cleanHistory.js
  63. 19
      app/ui-message/client/message.html
  64. 11
      app/ui-message/client/message.js
  65. 2
      app/ui-message/client/messageBox.js
  66. 2
      app/ui-sidenav/client/chatRoomItem.js
  67. 8
      app/ui-sidenav/client/roomList.js
  68. 50
      app/ui-sidenav/client/sidebarHeader.js
  69. 9
      app/ui-sidenav/client/sortlist.html
  70. 4
      app/ui-sidenav/client/sortlist.js
  71. 6
      app/ui-utils/client/lib/RoomManager.js
  72. 12
      app/ui-utils/client/lib/modal.html
  73. 5
      app/ui-utils/client/lib/modal.js
  74. 2
      app/ui/client/components/header/headerRoom.html
  75. 8
      app/ui/client/components/header/headerRoom.js
  76. 6
      app/ui/client/lib/chatMessages.js
  77. 5
      app/ui/client/views/app/room.js
  78. 2
      client/importPackages.js
  79. 1
      client/importsCss.js
  80. 29
      packages/rocketchat-i18n/i18n/de.i18n.json
  81. 53
      packages/rocketchat-i18n/i18n/en.i18n.json
  82. 4
      private/client/imports/general/variables.css
  83. 3
      private/public/icons.svg
  84. 2
      server/importPackages.js
  85. 1
      server/startup/migrations/index.js
  86. 118
      server/startup/migrations/v139.js
  87. 2
      tests/end-to-end/api/00-miscellaneous.js
  88. 16
      tests/end-to-end/ui/15-discussion.js
  89. 46
      tests/pageobjects/discussion.page.js
  90. 2
      tests/pageobjects/side-nav.page.js

@ -60,7 +60,7 @@ Meteor.methods({
}
if (room.prid) {
throw new Meteor.Error('error-action-not-allowed', 'Editing thread room is not allowed', {
throw new Meteor.Error('error-action-not-allowed', 'Editing discussion room is not allowed', {
method: 'saveRoomSettings',
action: 'Editing_room',
});

@ -0,0 +1,53 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Subscriptions } from '../../models/client';
import { settings } from '../../settings/client';
import { hasPermission } from '../../authorization/client';
import { MessageAction, modal } from '../../ui-utils/client';
import { t } from '../../utils/client';
const condition = (rid, uid) => {
if (!Subscriptions.findOne({ rid })) {
return false;
}
return uid !== Meteor.userId() ? hasPermission('start-discussion-other-user') : hasPermission('start-discussion');
};
Meteor.startup(function() {
Tracker.autorun(() => {
if (!settings.get('Discussion_enabled')) {
return MessageAction.removeButton('start-discussion');
}
MessageAction.addButton({
id: 'start-discussion',
icon: 'discussion',
label: 'Discussion_start',
context: ['message', 'message-mobile'],
async action() {
const [, message] = this._arguments;
modal.open({
title: t('Discussion_title'),
modifier: 'modal',
content: 'CreateDiscussion',
data: { rid: message.rid, message, onCreate() {
modal.close();
} },
confirmOnEnter: false,
showConfirmButton: false,
showCancelButton: false,
});
},
condition({ rid, u: { _id: uid }, drid, dcount }) {
if (drid || !isNaN(dcount)) {
return false;
}
return condition(rid, uid);
},
order: 0,
group: 'menu',
});
});
});

@ -0,0 +1,36 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { messageBox, modal } from '../../ui-utils/client';
import { t } from '../../utils/client';
import { settings } from '../../settings/client';
Meteor.startup(function() {
Tracker.autorun(() => {
if (!settings.get('Discussion_enabled')) {
return messageBox.actions.remove('Create_new', /start-discussion/);
}
messageBox.actions.add('Create_new', 'Discussion', {
id: 'start-discussion',
icon: 'discussion',
condition: () => true,
action(data) {
modal.open({
title: t('Discussion_title'),
modifier: 'modal',
content: 'CreateDiscussion',
data: {
...data,
onCreate() {
modal.close();
},
},
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,
});
},
});
});
});

@ -0,0 +1,16 @@
// Templates
import './views/creationDialog/CreateDiscussion';
import './views/DiscussionList';
import './views/DiscussionTabbar';
// Other UI extensions
import './lib/messageTypes/discussionMessage';
import './lib/discussionsOfRoom';
import './createDiscussionMessageAction';
import './discussionFromMessageBox';
import './tabBar';
import '../lib/discussionRoomType';
// Style
import './public/stylesheets/discussion.css';

@ -0,0 +1,3 @@
import { Mongo } from 'meteor/mongo';
export const DiscussionOfRoom = new Mongo.Collection('rocketchat_discussions_of_room');

@ -0,0 +1,17 @@
import { Meteor } from 'meteor/meteor';
import { MessageTypes } from '../../../../ui-utils/client';
Meteor.startup(function() {
MessageTypes.registerType({
id: 'discussion-created',
system: false,
message: 'discussion-created',
data(message) {
return {
// channelLink: `<a class="mention-link" data-channel= ${ message.channels[0]._id } title="">${ TAPi18n.__('discussion') }</a>`,
message: `<svg class="rc-icon" aria-hidden="true"><use xlink:href="#icon-discussion"></use></svg> ${ message.msg }`,
};
},
});
});

@ -0,0 +1,22 @@
.message-discussion {
display: flex;
padding: 0.5rem 0;
align-items: center;
}
.discussion-reply-lm {
padding: 4px 8px;
color: var(--color-gray);
font-size: 12px;
}
.discussions-list .load-more {
text-align: center;
text-transform: lowercase;
font-style: italic;
line-height: 40px;
}

@ -1,13 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { TabBar } from '../../ui-utils';
import { TabBar } from '../../ui-utils/client';
Meteor.startup(function() {
return TabBar.addButton({
groups: ['channel', 'group', 'direct'],
id: 'threads',
i18nTitle: 'Threads',
icon: 'thread',
template: 'threadsTabbar',
id: 'discussions',
i18nTitle: 'Discussions',
icon: 'discussion',
template: 'discussionsTabbar',
order: 10,
});
});

@ -1,7 +1,7 @@
<template name="ThreadList">
<template name="DiscussionList">
{{#if rooms}}
<h3 class="rooms-list__type">
{{_ "Threads"}}
{{_ "Discussion"}}
</h3>
<ul class="rooms-list__list">
{{#each room in rooms}} {{> chatRoomItem room }} {{/each}}

@ -1,9 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { ChatSubscription } from '../../../models';
import { getUserPreference } from '../../../utils';
import { settings } from '../../../settings';
Template.ThreadList.helpers({
import { ChatSubscription } from '../../../models/client';
import { getUserPreference } from '../../../utils/client';
import { settings } from '../../../settings/client';
import './DiscussionList.html';
Template.DiscussionList.helpers({
rooms() {
const user = Meteor.userId();
const sortBy = getUserPreference(user, 'sidebarSortby') || 'alphabetical';

@ -1,12 +1,12 @@
<template name="threadsTabbar">
<template name="discussionsTabbar">
{{#if Template.subscriptionsReady}}
{{#unless hasMessages}}
<div class="list-view threads-list flex-tab__header">
<h2>{{_ "No_threads_yet"}}</h2>
<div class="list-view discussions-list flex-tab__header">
<h2>{{_ "No_discussions_yet"}}</h2>
</div>
{{/unless}}
{{/if}}
<div class="flex-tab__result threads-list js-list">
<div class="flex-tab__result discussions-list js-list">
<ul class="list clearfix">
{{#each messages}}
{{#nrr nrrargs 'message' message}}{{/nrr}}

@ -1,20 +1,19 @@
import _ from 'underscore';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { ThreadsOfRoom } from '../lib/threadsOfRoom';
Template.threadsTabbar.helpers({
import { DiscussionOfRoom } from '../lib/discussionsOfRoom';
import './DiscussionTabbar.html';
Template.discussionsTabbar.helpers({
hasMessages() {
return ThreadsOfRoom.find({
return DiscussionOfRoom.find({
rid: this.rid,
}, {
sort: {
ts: -1,
},
}).count() > 0;
},
messages() {
return ThreadsOfRoom.find({
return DiscussionOfRoom.find({
rid: this.rid,
}, {
sort: {
@ -30,13 +29,13 @@ Template.threadsTabbar.helpers({
},
});
Template.threadsTabbar.onCreated(function() {
Template.discussionsTabbar.onCreated(function() {
this.hasMore = new ReactiveVar(true);
this.limit = new ReactiveVar(50);
return this.autorun(() => {
const data = Template.currentData();
return this.subscribe('threadsOfRoom', data.rid, this.limit.get(), () => {
if (ThreadsOfRoom.find({
return this.subscribe('discussionsOfRoom', data.rid, this.limit.get(), () => {
if (DiscussionOfRoom.find({
rid: data.rid,
}).count() < this.limit.get()) {
return this.hasMore.set(false);
@ -45,9 +44,9 @@ Template.threadsTabbar.onCreated(function() {
});
});
Template.threadsTabbar.events({
Template.discussionsTabbar.events({
'scroll .js-list': _.throttle(function(e, instance) {
if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get()) {
if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight - 10 && instance.hasMore.get()) {
return instance.limit.set(instance.limit.get() + 50);
}
}, 200),

@ -1,11 +1,11 @@
<template name="CreateThread">
<template name="CreateDiscussion">
<section class="create-channel">
<div class="create-channel__wrapper">
<p class="create-channel__description">{{_ "Threading_description"}}</p>
<form id="create-thread" class="create-channel__content">
<p class="create-channel__description">{{_ "Discussion_description"}}</p>
<form id="create-discussion" class="create-channel__content">
{{#unless disabled}}
{{> SearchCreateThread
{{> SearchCreateDiscussion
onClickTag=onClickTagRoom
deleteLastItem=deleteLastItemRoom
list=selectedRoom
@ -13,8 +13,8 @@
collection=roomCollection
field='name'
sort='name'
label="Thread_target_channel"
placeholder="Thread_target_channel_description"
label="Discussion_target_channel"
placeholder="Discussion_target_channel_description"
name="parentChannel"
disabled=disabled
selector=roomSelector
@ -25,48 +25,49 @@
}} {{else}}
<div class="rc-input" disabled>
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Thread_target_channel"}}</div>
<div class="rc-input__title">{{_ "Discussion_target_channel"}}</div>
<div class="rc-input__wrapper">
<input disabled name="parentChannel" id="parentChannel" value={{parentChannel}} class="rc-input__element" />
</div>
</label>
</div>
{{/unless}}
{{> SearchCreateThread
onClickTag=onClickTagUser
deleteLastItem=deleteLastItemUser
list=selectedUsers
onSelect=onSelectUser
collection='UserAndRoom'
subscription='userAutocomplete'
field='username'
sort='username'
label="Invite_Users"
placeholder="Username_Placeholder"
name="users"
icon="at"
noMatchTemplate="userSearchEmpty"
templateItem="popupList_item_default"
modifier=userModifier
}}
<div class="create-channel__inputs">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Thread_name"}}</div>
<div class="rc-input__title">{{_ "Discussion_name"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="thread"}}
{{> icon block="rc-input__icon-svg" icon="discussion"}}
</div>
<input name="thread_name" id="thread_name" class="rc-input__element" placeholder="{{_ 'New_thread_name'}}"
<input name="discussion_name" id="discussion_name" class="rc-input__element" placeholder="{{_ 'New_discussion_name'}}"
maxlength="{{maxMessageLength}}" value="{{channelName}}"/>
</div>
</label>
</div>
{{> SearchCreateDiscussion
onClickTag=onClickTagUser
deleteLastItem=deleteLastItemUser
list=selectedUsers
onSelect=onSelectUser
collection='UserAndRoom'
subscription='userAutocomplete'
field='username'
sort='username'
label="Invite_Users"
placeholder="Username_Placeholder"
name="users"
icon="at"
noMatchTemplate="userSearchEmpty"
templateItem="popupList_item_default"
modifier=userModifier
}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Threading_first_message_title"}}</div>
<div class="rc-input__title">{{_ "Discussion_first_message_title"}}</div>
<div class="rc-input__wrapper">
<textarea name="thread_message" id="thread_message" class="rc-input__element" placeholder="{{_ 'New_thread_first_message'}}"
<textarea rows="5" name="discussion_message" id="discussion_message" class="rc-input__element" placeholder="{{_ 'New_discussion_first_message'}}"
maxlength="{{maxMessageLength}}"></textarea>
</div>
</label>
@ -74,13 +75,13 @@
</div>
</form>
<div class="rc-input">
<button form="create-thread" class="rc-button rc-button--primary js-save-thread {{createIsDisabled}}" {{createIsDisabled}}>{{_ " Create "}}</button>
<button form="create-discussion" class="rc-button rc-button--primary js-save-discussion {{createIsDisabled}}" {{createIsDisabled}}>{{_ " Create "}}</button>
</div>
</div>
</section>
</template>
<template name="SearchCreateThread">
<template name="SearchCreateDiscussion">
<div class="rc-input" id='search-{{name}}' {{disabled}}>
<label class="rc-input__label">
<div class="rc-input__title">{{_ label}}</div>

@ -1,19 +1,19 @@
import { Meteor } from 'meteor/meteor';
import { roomTypes } from '../../../../utils';
import { callbacks } from '../../../../callbacks';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { AutoComplete } from 'meteor/mizzao:autocomplete';
import { ChatRoom, ChatSubscription } from '../../../../models';
import { Blaze } from 'meteor/blaze';
import { call } from '../../../../ui-utils';
import { TAPi18n } from 'meteor/tap:i18n';
import toastr from 'toastr';
import { roomTypes } from '../../../../utils/client';
import { callbacks } from '../../../../callbacks/client';
import { ChatRoom, ChatSubscription } from '../../../../models/client';
import { call } from '../../../../ui-utils/client';
Template.CreateThread.helpers({
import './CreateDiscussion.html';
Template.CreateDiscussion.helpers({
onSelectUser() {
return Template.instance().onSelectUser;
},
@ -25,14 +25,11 @@ Template.CreateThread.helpers({
targetChannelText() {
const instance = Template.instance();
const parentChannel = instance.parentChannel.get();
return parentChannel && `${ TAPi18n.__('Thread_target_channel_prefix') } "${ parentChannel }"`;
return parentChannel && `${ TAPi18n.__('Discussion_target_channel_prefix') } "${ parentChannel }"`;
},
createIsDisabled() {
const instance = Template.instance();
if (instance.reply.get() && instance.parentChannel.get()) {
return '';
}
return 'disabled';
const { parentChannel, discussionName } = Template.instance();
return parentChannel.get() && discussionName.get() ? '' : 'disabled';
},
parentChannel() {
const instance = Template.instance();
@ -41,7 +38,7 @@ Template.CreateThread.helpers({
selectedUsers() {
const myUsername = Meteor.user().username;
const { message } = this;
const users = Template.instance().selectedUsers.get();
const users = Template.instance().selectedUsers.get().map((e) => e);
if (message) {
users.unshift(message.u);
}
@ -85,24 +82,24 @@ Template.CreateThread.helpers({
};
},
channelName() {
return Template.instance().threadName.get();
return Template.instance().discussionName.get();
},
});
Template.CreateThread.events({
'input #thread_name'(e, t) {
t.threadName.set(e.target.value);
Template.CreateDiscussion.events({
'input #discussion_name'(e, t) {
t.discussionName.set(e.target.value);
},
'input #thread_message'(e, t) {
'input #discussion_message'(e, t) {
const { value } = e.target;
t.reply.set(value);
},
async 'submit #create-thread, click .js-save-thread'(event, instance) {
async 'submit #create-discussion, click .js-save-discussion'(event, instance) {
event.preventDefault();
const parentChannel = instance.parentChannel.get();
const { pmid } = instance;
const t_name = instance.threadName.get();
const t_name = instance.discussionName.get();
const users = instance.selectedUsers.get().map(({ username }) => username).filter((value, index, self) => self.indexOf(value) === index);
const prid = instance.parentChannelId.get();
@ -112,9 +109,9 @@ Template.CreateThread.events({
const errorText = TAPi18n.__('Invalid_room_name', `${ parentChannel }...`);
return toastr.error(errorText);
}
const result = await call('createThread', { prid, pmid, t_name, reply, users });
const result = await call('createDiscussion', { prid, pmid, t_name, reply, users });
// callback to enable tracking
callbacks.run('afterCreateThread', Meteor.user(), result);
callbacks.run('afterDiscussion', Meteor.user(), result);
if (instance.data.onCreate) {
instance.data.onCreate(result);
@ -124,16 +121,18 @@ Template.CreateThread.events({
},
});
Template.CreateThread.onRendered(function() {
this.find(this.data.rid ? '#thread_name' : '#parentChannel').focus();
Template.CreateDiscussion.onRendered(function() {
this.find(this.data.rid ? '#discussion_name' : '#parentChannel').focus();
});
const suggestName = (name, msg) => [name, msg].filter((e) => e).join(' - ').substr(0, 140);
Template.CreateThread.onCreated(function() {
Template.CreateDiscussion.onCreated(function() {
const { rid, message: msg } = this.data;
const parentRoom = rid && ChatSubscription.findOne({ rid });
// if creating a thread from inside a thread, uses the same channel as parent channel
// if creating a discussion from inside a discussion, uses the same channel as parent channel
const room = parentRoom && parentRoom.prid ? ChatSubscription.findOne({ rid: parentRoom.prid }) : parentRoom;
if (room) {
@ -141,7 +140,7 @@ Template.CreateThread.onCreated(function() {
}
const roomName = room && roomTypes.getRoomName(room.t, room);
this.threadName = new ReactiveVar(suggestName(roomName, msg && msg.msg));
this.discussionName = new ReactiveVar(suggestName(roomName, msg && msg.msg));
this.pmid = msg && msg._id;
@ -174,7 +173,6 @@ Template.CreateThread.onCreated(function() {
this.parentChannelId.set(room && (room.rid || room._id));
});
this.selectedUsers = new ReactiveVar([]);
this.onSelectUser = ({ item: user }) => {
@ -182,6 +180,9 @@ Template.CreateThread.onCreated(function() {
return;
}
if (user.username === Meteor.user().username) {
return;
}
const users = this.selectedUsers.get();
if (!users.find((u) => user.username === u.username)) {
this.selectedUsers.set([...users, user]);
@ -196,9 +197,8 @@ Template.CreateThread.onCreated(function() {
this.selectedUsers.set(arr);
});
// callback to allow setting a parent Channel or e. g. tracking the event using Piwik or GA
const { parentChannel, reply } = callbacks.run('openThreadCreationScreen') || {};
const { parentChannel, reply } = callbacks.run('openDiscussionCreationScreen') || {};
if (parentChannel) {
this.parentChannel.set(parentChannel);
@ -208,7 +208,7 @@ Template.CreateThread.onCreated(function() {
}
});
Template.SearchCreateThread.helpers({
Template.SearchCreateDiscussion.helpers({
list() {
return this.list;
},
@ -234,7 +234,7 @@ Template.SearchCreateThread.helpers({
},
});
Template.SearchCreateThread.events({
Template.SearchCreateDiscussion.events({
'input input'(e, t) {
const input = e.target;
const position = input.selectionEnd || input.selectionStart;
@ -267,7 +267,7 @@ Template.SearchCreateThread.events({
return onClickTag & onClickTag(Blaze.getData(target));
},
});
Template.SearchCreateThread.onRendered(function() {
Template.SearchCreateDiscussion.onRendered(function() {
const { name } = this.data;
@ -275,7 +275,7 @@ Template.SearchCreateThread.onRendered(function() {
this.ac.$element = $(this.ac.element);
});
Template.SearchCreateThread.onCreated(function() {
Template.SearchCreateDiscussion.onCreated(function() {
this.filter = new ReactiveVar('');
this.selected = new ReactiveVar([]);
this.onClickTag = this.data.onClickTag;

@ -1,21 +1,21 @@
import { Meteor } from 'meteor/meteor';
import { RoomTypeConfig, roomTypes, getUserPreference } from '../../utils';
export class ThreadRoomType extends RoomTypeConfig {
export class DiscussionRoomType extends RoomTypeConfig {
constructor() {
super({
identifier: 't',
order: 25,
label: 'Threads',
label: 'Discussion',
});
// we need a custom template in order to have a custom query showing the subscriptions to threads
this.customTemplate = 'ThreadList';
// we need a custom template in order to have a custom query showing the subscriptions to discussions
this.customTemplate = 'DiscussionList';
}
condition() {
return getUserPreference(Meteor.userId(), 'sidebarShowThreads');
return getUserPreference(Meteor.userId(), 'sidebarShowDiscussion');
}
}
roomTypes.add(new ThreadRoomType());
roomTypes.add(new DiscussionRoomType());

@ -0,0 +1,38 @@
import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings';
Meteor.startup(() => {
settings.addGroup('Discussion', function() {
// the channel for which discussions are created if none is explicitly chosen
this.add('Discussion_enabled', true, {
group: 'Discussion',
i18nLabel: 'Enable',
type: 'boolean',
public: true,
});
});
settings.add('Accounts_Default_User_Preferences_sidebarShowDiscussion', true, {
group: 'Accounts',
section: 'Accounts_Default_User_Preferences',
type: 'boolean',
public: true,
i18nLabel: 'Group_discussions',
});
const globalQuery = {
_id: 'RetentionPolicy_Enabled',
value: true,
};
settings.add('RetentionPolicy_DoNotExcludeDiscussion', true, {
group: 'RetentionPolicy',
section: 'Global Policy',
type: 'boolean',
public: true,
i18nLabel: 'RetentionPolicy_DoNotExcludeDiscussion',
i18nDescription: 'RetentionPolicy_DoNotExcludeDiscussion_Description',
enableQuery: globalQuery,
});
});

@ -1,15 +1,16 @@
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../callbacks';
import { Subscriptions } from '../../../models';
import { callbacks } from '../../../callbacks/server';
import { Subscriptions } from '../../../models/server';
callbacks.add('beforeSaveMessage', (message, room) => {
// abort if room is not a thread
// abort if room is not a discussion
if (!room || !room.prid) {
return message;
}
// check if user already joined the thread
// check if user already joined the discussion
const sub = Subscriptions.findOneByRoomIdAndUserId(room._id, message.u._id, { fields: { _id: 1 } });
if (sub) {
return message;

@ -0,0 +1,27 @@
import { callbacks } from '../../../callbacks/server';
import { Messages, Rooms } from '../../../models/server';
import { deleteRoom } from '../../../lib/server';
/**
* We need to propagate the writing of new message in a discussion to the linking
* system message
*/
callbacks.add('afterSaveMessage', function(message, { _id, prid } = {}) {
if (prid) {
Messages.refreshDiscussionMetadata({ rid: _id }, message);
}
return message;
}, callbacks.priority.LOW, 'PropagateDiscussionMetadata');
callbacks.add('afterDeleteMessage', function(message, { _id, prid } = {}) {
if (prid) {
Messages.refreshDiscussionMetadata({ rid: _id }, message);
}
if (message.drid) {
deleteRoom(message.drid);
}
return message;
}, callbacks.priority.LOW, 'PropagateDiscussionMetadata');
callbacks.add('afterDeleteRoom', function(rid) {
Rooms.find({ prid: rid }, { fields: { _id: 1 } }).forEach(({ _id }) => deleteRoom(_id));
}, 'DeleteDiscussionChain');

@ -0,0 +1,14 @@
import './config';
import './authorization';
import './permissions';
import './hooks/joinDiscussionOnMessage';
import './hooks/propagateDiscussionMetadata';
import './publications/discussionParentAutocomplete';
import './publications/discussionsOfRoom';
// Methods
import './methods/createDiscussion';
// Lib
import '../lib/discussionRoomType';

@ -0,0 +1,133 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { hasAtLeastOnePermission, canAccessRoom } from '../../../authorization/server';
import { Messages, Rooms } from '../../../models/server';
import { createRoom, addUserToRoom, sendMessage, attachMessage } from '../../../lib/server';
import { settings } from '../../../settings/server';
const getParentRoom = (rid) => {
const room = Rooms.findOne(rid);
return room && (room.prid ? Rooms.findOne(room.prid, { fields: { _id: 1 } }) : room);
};
const createDiscussionMessage = (rid, user, drid, msg, message_embedded) => {
const welcomeMessage = {
msg,
rid,
drid,
attachments: [message_embedded].filter((e) => e),
};
return Messages.createWithTypeRoomIdMessageAndUser('discussion-created', rid, '', user, welcomeMessage);
};
const mentionMessage = (rid, { _id, username, name }, message_embedded) => {
const welcomeMessage = {
rid,
u: { _id, username, name },
ts: new Date(),
_updatedAt: new Date(),
attachments: [message_embedded].filter((e) => e),
};
return Messages.insert(welcomeMessage);
};
const create = ({ prid, pmid, t_name, reply, users }) => {
// if you set both, prid and pmid, and the rooms doesnt match... should throw an error)
let message = false;
if (pmid) {
message = Messages.findOne({ _id: pmid });
if (prid) {
if (prid !== getParentRoom(message.rid)._id) {
throw new Meteor.Error('error-invalid-arguments', { method: 'DiscussionCreation' });
}
} else {
prid = message.rid;
}
}
if (!prid) {
throw new Meteor.Error('error-invalid-arguments', { method: 'DiscussionCreation' });
}
const p_room = Rooms.findOne(prid);
if (p_room.prid) {
throw new Meteor.Error('error-nested-discussion', 'Cannot create nested discussions', { method: 'DiscussionCreation' });
}
const user = Meteor.user();
if (!canAccessRoom(p_room, user)) {
throw new Meteor.Error('error-not-allowed', { method: 'DiscussionCreation' });
}
if (pmid) {
const discussionAlreadyExists = Rooms.findOne({
prid,
pmid,
}, {
fields: { _id: 1 },
});
if (discussionAlreadyExists) { // do not allow multiple discussions to the same message'\
addUserToRoom(discussionAlreadyExists._id, user);
return discussionAlreadyExists;
}
}
const name = Random.id();
// auto invite the replied message owner
const invitedUsers = message ? [message.u.username, ...users] : users;
// discussions are always created as private groups
const discussion = createRoom('p', name, user.username, [...new Set(invitedUsers)], false, {
fname: t_name,
description: message.msg, // TODO discussions remove
topic: p_room.name, // TODO discussions remove
prid,
});
if (pmid) {
mentionMessage(discussion._id, user, attachMessage(message, p_room));
createDiscussionMessage(message.rid, user, discussion._id, t_name, attachMessage(message, p_room));
} else {
createDiscussionMessage(prid, user, discussion._id, t_name);
}
if (reply) {
sendMessage(user, { msg: reply }, discussion);
}
return discussion;
};
Meteor.methods({
/**
* Create discussion by room or message
* @constructor
* @param {string} prid - Parent Room Id - The room id, optional if you send pmid.
* @param {string} pmid - Parent Message Id - Create the discussion by a message, optional.
* @param {string} reply - The reply, optional
* @param {string} t_name - discussion name
* @param {string[]} users - users to be added
*/
createDiscussion({ prid, pmid, t_name, reply, users }) {
if (!settings.get('Discussion_enabled')) {
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
}
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'DiscussionCreation' });
}
if (!hasAtLeastOnePermission(uid, ['start-discussion', 'start-discussion-other-user'])) {
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
}
return create({ uid, prid, pmid, t_name, reply, users });
},
});

@ -2,10 +2,10 @@ import { Meteor } from 'meteor/meteor';
import { Permissions } from '../../models';
Meteor.startup(() => {
// Add permissions for threading
// Add permissions for discussion
const permissions = [
{ _id: 'start-thread', roles: ['admin', 'user', 'expert', 'guest'] },
{ _id: 'start-thread-other-user', roles: ['admin', 'user', 'expert', 'owner'] },
{ _id: 'start-discussion', roles: ['admin', 'user', 'expert', 'guest'] },
{ _id: 'start-discussion-other-user', roles: ['admin', 'user', 'expert', 'owner'] },
];
for (const permission of permissions) {

@ -1,9 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { Rooms } from '../../../models';
import { hasPermission } from '../../../authorization';
import { Rooms } from '../../../models/server';
import { hasPermission } from '../../../authorization/server';
Meteor.publish('threadParentAutocomplete', function(selector) {
Meteor.publish('discussionParentAutocomplete', function(selector) {
if (!this.userId) {
return this.ready();
}
@ -24,7 +24,7 @@ Meteor.publish('threadParentAutocomplete', function(selector) {
},
};
const cursorHandle = Rooms.findThreadParentByNameStarting(selector.name, options).observeChanges({
const cursorHandle = Rooms.findDiscussionParentByNameStarting(selector.name, options).observeChanges({
added(_id, record) {
return pub.added('autocompleteRecords', _id, record);
},

@ -0,0 +1,32 @@
import { Meteor } from 'meteor/meteor';
import { Messages } from '../../../models/server';
Meteor.publish('discussionsOfRoom', function(rid, limit = 50) {
if (!this.userId) {
return this.ready();
}
const publication = this;
if (!Meteor.call('canAccessRoom', rid, this.userId)) {
return this.ready();
}
const cursorHandle = Messages.find({ rid, drid: { $exists: true } }, { sort: { ts: -1 }, limit }).observeChanges({
added(_id, record) {
return publication.added('rocketchat_discussions_of_room', _id, record);
},
changed(_id, record) {
return publication.changed('rocketchat_discussions_of_room', _id, record);
},
removed(_id) {
return publication.removed('rocketchat_discussions_of_room', _id);
},
});
this.ready();
return this.onStop(function() {
return cursorHandle.stop();
});
});

@ -31,7 +31,7 @@ export class PrivateRoomType extends RoomTypeConfig {
getIcon(roomData) {
if (roomData.prid) {
return 'thread';
return 'discussion';
}
return this.icon;
}

@ -31,7 +31,7 @@ export class PublicRoomType extends RoomTypeConfig {
getIcon(roomData) {
if (roomData.prid) {
return 'thread';
return 'discussion';
}
return this.icon;
}

@ -45,7 +45,7 @@ export const addUserToRoom = function(rid, user, inviter, silenced) {
},
});
} else if (room.prid) {
Messages.createUserJoinWithRoomIdAndUserThread(rid, user, { ts: now });
Messages.createUserJoinWithRoomIdAndUserDiscussion(rid, user, { ts: now });
} else {
Messages.createUserJoinWithRoomIdAndUser(rid, user, { ts: now });
}

@ -4,7 +4,7 @@ import { Messages, Rooms } from '../../../models';
import { Notifications } from '../../../notifications';
import { deleteRoom } from './deleteRoom';
export const cleanRoomHistory = function({ rid, latest = new Date(), oldest = new Date('0001-01-01T00:00:00Z'), inclusive = true, limit = 0, excludePinned = true, ignoreThreads = true, filesOnly = false, fromUsers = [] }) {
export const cleanRoomHistory = function({ rid, latest = new Date(), oldest = new Date('0001-01-01T00:00:00Z'), inclusive = true, limit = 0, excludePinned = true, ignoreDiscussion = true, filesOnly = false, fromUsers = [] }) {
const gt = inclusive ? '$gte' : '$gt';
const lt = inclusive ? '$lte' : '$lt';
@ -16,7 +16,7 @@ export const cleanRoomHistory = function({ rid, latest = new Date(), oldest = ne
Messages.findFilesByRoomIdPinnedTimestampAndUsers(
rid,
excludePinned,
ignoreThreads,
ignoreDiscussion,
ts,
fromUsers,
{ fields: { 'file._id': 1, pinned: 1 }, limit }
@ -31,18 +31,18 @@ export const cleanRoomHistory = function({ rid, latest = new Date(), oldest = ne
return fileCount;
}
if (!ignoreThreads) {
Messages.findThreadByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, fromUsers, { fields: { trid: 1 }, ...(limit && { limit }) }).fetch()
.forEach(({ trid }) => deleteRoom(trid));
if (!ignoreDiscussion) {
Messages.findDiscussionByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, fromUsers, { fields: { drid: 1 }, ...(limit && { limit }) }).fetch()
.forEach(({ drid }) => deleteRoom(drid));
}
const count = Messages.removeByIdPinnedTimestampLimitAndUsers(rid, excludePinned, ignoreThreads, ts, limit, fromUsers);
const count = Messages.removeByIdPinnedTimestampLimitAndUsers(rid, excludePinned, ignoreDiscussion, ts, limit, fromUsers);
if (count) {
Rooms.resetLastMessageById(rid);
Notifications.notifyRoom(rid, 'deleteMessageBulk', {
rid,
excludePinned,
ignoreThreads,
ignoreDiscussion,
ts,
users: fromUsers,
});

@ -4,7 +4,7 @@ import { hasPermission } from '../../../authorization';
import { cleanRoomHistory } from '../functions';
Meteor.methods({
cleanRoomHistory({ roomId, latest, oldest, inclusive = true, limit, excludePinned = false, ignoreThreads = true, filesOnly = false, fromUsers = [] }) {
cleanRoomHistory({ roomId, latest, oldest, inclusive = true, limit, excludePinned = false, ignoreDiscussion = true, filesOnly = false, fromUsers = [] }) {
check(roomId, String);
check(latest, Date);
check(oldest, Date);
@ -24,6 +24,6 @@ Meteor.methods({
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' });
}
return cleanRoomHistory({ rid: roomId, latest, oldest, inclusive, limit, excludePinned, ignoreThreads, filesOnly, fromUsers });
return cleanRoomHistory({ rid: roomId, latest, oldest, inclusive, limit, excludePinned, ignoreDiscussion, filesOnly, fromUsers });
},
});

@ -6,7 +6,8 @@
{{else}}
{{pretext}}
{{/if}}
<div class="attachment-block">
<div class="color-primary-font-color {{# unless $eq color 'none'}}attachment-block{{/unless}}">
<div class="attachment-block-border background-info-font-color" style="background-color: {{color}}"></div>
{{#if author_name}}
{{#if author_link}}
@ -56,7 +57,7 @@
{{#if title_link}}
<a href="{{title_link}}" target="_blank" rel="noopener noreferrer">{{title}}</a>
{{#if title_link_download}}
<a class="attachment-download-icon" href="{{title_link}}" target="_blank" download="" rel="noopener noreferrer">{{> icon icon="download"}}</a>
<a class="attachment-download-icon rc-tooltip" aria-label="{{_ 'Download'}}" href="{{title_link}}" target="_blank" download="" rel="noopener noreferrer">{{> icon icon="download"}}</a>
{{/if}}
{{else}}
{{title}}

@ -89,6 +89,8 @@ html.rtl .attachment {
display: flex;
margin-top: 4px;
align-items: center;
flex-wrap: wrap;
& .attachment-field {

@ -26,8 +26,8 @@ export class Messages extends Base {
this.tryEnsureIndex({ slackBotId: 1, slackTs: 1 }, { sparse: true });
this.tryEnsureIndex({ unread: 1 }, { sparse: true });
// threads
this.tryEnsureIndex({ trid: 1 }, { sparse: true });
// discussions
this.tryEnsureIndex({ drid: 1 }, { sparse: true });
}
setReactions(messageId, reactions) {
@ -152,7 +152,7 @@ export class Messages extends Base {
return this.find(query, { fields: { 'file._id': 1 }, ...options });
}
findFilesByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ignoreThreads = true, ts, users = [], options = {}) {
findFilesByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ignoreDiscussion = true, ts, users = [], options = {}) {
const query = {
rid,
ts,
@ -163,8 +163,8 @@ export class Messages extends Base {
query.pinned = { $ne: true };
}
if (ignoreThreads) {
query.trid = { $exists: 0 };
if (ignoreDiscussion) {
query.drid = { $exists: 0 };
}
if (users.length) {
@ -174,11 +174,11 @@ export class Messages extends Base {
return this.find(query, { fields: { 'file._id': 1 }, ...options });
}
findThreadByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, users = [], options = {}) {
findDiscussionByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, users = [], options = {}) {
const query = {
rid,
ts,
trid: { $exists: 1 },
drid: { $exists: 1 },
};
if (excludePinned) {
@ -763,7 +763,7 @@ export class Messages extends Base {
return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData);
}
createUserJoinWithRoomIdAndUserThread(roomId, user, extraData) {
createUserJoinWithRoomIdAndUserDiscussion(roomId, user, extraData) {
const message = user.username;
return this.createWithTypeRoomIdMessageAndUser('ut', roomId, message, user, extraData);
}
@ -860,7 +860,7 @@ export class Messages extends Base {
return this.remove(query);
}
removeByIdPinnedTimestampLimitAndUsers(rid, pinned, ignoreThreads = true, ts, limit, users = []) {
removeByIdPinnedTimestampLimitAndUsers(rid, pinned, ignoreDiscussion = true, ts, limit, users = []) {
const query = {
rid,
ts,
@ -870,8 +870,8 @@ export class Messages extends Base {
query.pinned = { $ne: true };
}
if (ignoreThreads) {
query.trid = { $exists: 0 };
if (ignoreDiscussion) {
query.drid = { $exists: 0 };
}
if (users.length) {
@ -965,17 +965,17 @@ export class Messages extends Base {
}
/**
* Copy metadata from the thread to the system message in the parent channel
* which links to the thread.
* Copy metadata from the discussion to the system message in the parent channel
* which links to the discussion.
* Since we don't pass this metadata into the model's function, it is not a subject
* to race conditions: If multiple updates occur, the current state will be updated
* only if the new state of the thread room is really newer.
* only if the new state of the discussion room is really newer.
*/
refreshThreadMetadata({ rid }) {
refreshDiscussionMetadata({ rid }) {
if (!rid) {
return false;
}
const { lm, msgs: count } = Rooms.findOneById(rid, {
const { lm: dlm, msgs: dcount } = Rooms.findOneById(rid, {
fields: {
msgs: 1,
lm: 1,
@ -983,21 +983,13 @@ export class Messages extends Base {
});
const query = {
trid: rid,
drid: rid,
};
return this.update(query, {
$set: {
'attachments.0.fields': [
{
type: 'messageCounter',
count,
},
{
type: 'lastMessageAge',
lm,
},
],
dcount,
dlm,
},
}, { multi: 1 });
}

@ -19,7 +19,7 @@ export class Rooms extends Base {
this.tryEnsureIndex({ departmentId: 1 }, { sparse: 1 });
this.tryEnsureIndex({ ts: 1 });
// threads
// discussions
this.tryEnsureIndex({ prid: 1 });
}
@ -1369,8 +1369,8 @@ export class Rooms extends Base {
}
// ############################
// Threads
findThreadParentByNameStarting(name, options) {
// Discussion
findDiscussionParentByNameStarting(name, options) {
const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i');
const query = {

@ -21,7 +21,7 @@ function job() {
const now = new Date();
const filesOnly = settings.get('RetentionPolicy_FilesOnly');
const excludePinned = settings.get('RetentionPolicy_ExcludePinned');
const ignoreThreads = settings.get('RetentionPolicy_DoNotExcludeThreads');
const ignoreDiscussion = settings.get('RetentionPolicy_DoNotExcludeDiscussion');
// get all rooms with default values
types.forEach((type) => {
@ -37,7 +37,7 @@ function job() {
],
'retention.overrideGlobal': { $ne: true },
}, { fields : { _id: 1 } }).forEach(({ _id: rid }) => {
cleanRoomHistory({ rid, latest, oldest, filesOnly, excludePinned, ignoreThreads });
cleanRoomHistory({ rid, latest, oldest, filesOnly, excludePinned, ignoreDiscussion });
});
});
@ -49,7 +49,7 @@ function job() {
}).forEach((room) => {
const { maxAge = 30, filesOnly, excludePinned } = room.retention;
const latest = new Date(now.getTime() - maxAge * toDays);
cleanRoomHistory({ rid: room._id, latest, oldest, filesOnly, excludePinned, ignoreThreads });
cleanRoomHistory({ rid: room._id, latest, oldest, filesOnly, excludePinned, ignoreDiscussion });
});
lastPrune = new Date(now.getTime() - gracePeriod);
}

@ -10,6 +10,8 @@
padding: 0 0.25rem;
cursor: pointer;
justify-content: center;
}

@ -7,8 +7,6 @@
height: auto;
max-height: 100%;
padding: 0;
animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
border: none;

@ -24,8 +24,8 @@
}
&__switches,
&__inputs:not(:only-child),
& .rc-input:not(:only-child) {
&__inputs:not(:only-of-type),
& .rc-input:not(:only-of-type) {
margin-bottom: var(--create-channel-gap-between-elements);
}

@ -94,6 +94,8 @@
overflow-x: hidden;
overflow-y: auto;
flex: 1;
margin: 0 -1.5rem;
padding: 0 1.5rem;

@ -18,8 +18,8 @@
display: flex;
min-height: 40px;
max-height: 40px;
height: 40px;
padding: 0 1.5rem;
cursor: pointer;
@ -120,7 +120,10 @@
}
&--small {
height: var(--button-height-small);
padding: var(--button-padding-small);
font-size: var(--button-text-size-small);
}
&--square {

@ -1,3 +1,8 @@
textarea.rc-input__element {
font-family: inherit;
line-height: 0.5rem 1rem;
}
.rc-input {
position: relative;
@ -45,11 +50,6 @@
}
}
textarea.&__element {
font-family: inherit;
line-height: 2rem;
}
&__element {
width: 100%;
padding: 0 1rem;

@ -2878,14 +2878,6 @@ rc-old select,
line-height: 40px;
}
& button {
font-weight: 400;
&:hover {
text-decoration: underline;
}
}
& .body {
transition: opacity 1s linear;

@ -89,7 +89,9 @@
*/
--button-square-size: 36px;
--button-padding: 0.782rem;
--button-padding-small: 0.5rem;
--button-padding-small: 0 0.5rem;
--button-height-small: 28px;
--button-text-size-small: 13px;
--button-text-size: var(--input-font-size);
--button-border-width: var(--border);
--button-border-radius: var(--border-radius);

@ -1,49 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Subscriptions } from '../../models';
import { settings } from '../../settings';
import { hasPermission } from '../../authorization';
import { MessageAction, modal } from '../../ui-utils';
const condition = (rid, uid) => {
if (!Subscriptions.findOne({ rid })) {
return false;
}
return uid !== Meteor.userId() ? hasPermission('start-thread-other-user') : hasPermission('start-thread');
};
Meteor.startup(function() {
Tracker.autorun(() => {
if (settings.get('Thread_from_context_menu') !== 'button') {
return MessageAction.removeButton('start-thread');
}
MessageAction.addButton({
id: 'start-thread',
icon: 'thread',
label: 'Thread_start',
context: ['message', 'message-mobile'],
async action() {
const [, message] = this._arguments;
modal.open({
content: 'CreateThread',
data: { rid: message.rid, message, onCreate() {
modal.close();
} },
showConfirmButton: false,
showCancelButton: false,
});
},
condition({ rid, u: { _id: uid }, attachments }) {
if (attachments && attachments[0] && attachments[0].fields && attachments[0].fields[0].type === 'messageCounter') {
return false;
}
return condition(rid, uid);
},
order: 0,
group: 'menu',
});
});
});

@ -1,20 +0,0 @@
// Templates
import './views/creationDialog/CreateThread.html';
import './views/creationDialog/CreateThread';
import './views/ThreadList.html';
import './views/ThreadList';
import './views/ThreadsTabbar.html';
import './views/ThreadsTabbar';
import './views/fieldTypeThreadReplyCounter.html';
import './views/fieldTypeThreadReplyCounter';
import './views/fieldTypeThreadLastMessageAge.html';
import './views/fieldTypeThreadLastMessageAge';
// Other UI extensions
import './lib/messageTypes/threadMessage';
import './lib/threadsOfRoom';
import './createThreadMessageAction';
import './threadFromMessageBox';
import './tabBar';
import '../lib/threadRoomType';

@ -1,32 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/tap:i18n';
import { MessageTypes } from '../../../../ui-utils';
Meteor.startup(function() {
MessageTypes.registerType({
id: 'thread-created',
system: true,
message: 'thread-created',
data(message) {
return {
// channelLink: `<a class="mention-link" data-channel= ${ message.channels[0]._id } title="">${ TAPi18n.__('thread') }</a>`,
message: message.msg,
username: `<a class="mention-link" data-username=${ message.u.username } title="">@${ message.u.username }</a>`,
};
},
});
MessageTypes.registerType({
id: 'thread-welcome',
system: true,
message: 'thread-welcome',
data(message) {
const threadChannelName = TAPi18n.__('a_direct_message');
return {
parentChannel: `<a class="mention-link" data-channel= ${ threadChannelName } title="">${ threadChannelName }</a>`,
username: `<a class="mention-link" data-username= ${ message.mentions[0].name } title="">@${ message.mentions[0].name }</a>`,
};
},
});
});

@ -1,3 +0,0 @@
import { Mongo } from 'meteor/mongo';
export const ThreadsOfRoom = new Mongo.Collection('rocketchat_threads_of_room');

@ -1,34 +0,0 @@
.attachment-fields button {
min-height: auto;
padding: 3px;
font-weight: normal;
}
.attachment-fields button:hover {
text-decoration: none;
}
.attachment-fields button.no-replies {
opacity: 0.4;
}
.threads-list .empty {
margin-top: 60px;
text-align: center;
color: #7f7f7f;
}
.threads-list .load-more {
text-align: center;
text-transform: lowercase;
font-style: italic;
line-height: 40px;
}
.threads-list .load-more .load-more-loading {
color: #aaaaaa;
}

@ -1,33 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { messageBox, modal } from '../../ui-utils';
import { settings } from '../../settings';
Meteor.startup(function() {
Tracker.autorun(() => {
if (settings.get('Thread_from_context_menu') !== 'button') {
return messageBox.actions.remove('Create_new', /start-thread/);
}
messageBox.actions.add('Create_new', 'Thread', {
id: 'start-thread',
icon: 'thread',
condition: () => true,
action(data) {
modal.open({
// title: t('Message_info'),
content: 'CreateThread',
data: {
...data,
onCreate() {
modal.close();
},
},
showConfirmButton: false,
showCancelButton: false,
// confirmButtonText: t('Close'),
});
},
});
});
});

@ -1,7 +0,0 @@
<template name="LastMessageAge">
{{#with field}}
{{#if lastMessageAge}}
<button class="rc-tags__tag rc-tags--no-icon">{{lastMessageAge}}</button>
{{/if}}
{{/with}}
</template>

@ -1,12 +0,0 @@
import { registerFieldTemplate } from '../../../message-attachments';
import { Template } from 'meteor/templating';
import moment from 'moment';
Template.LastMessageAge.helpers({
lastMessageAge() {
const lastMessageTimestamp = Template.instance().data.field.lm;
return lastMessageTimestamp && moment(lastMessageTimestamp).format('LLL');
},
});
registerFieldTemplate('lastMessageAge', 'LastMessageAge', {});

@ -1,19 +0,0 @@
<template name="MessageCounter">
{{#with field}}
{{#if hasReplies}}
<button
class="js-navigate-to-thread rc-tags__tag rc-tags--no-icon"
data-rid={{roomId}}
>
<span class='reply-counter'>{{replyCount}}</span>&nbsp;{{_ i18nKeyReply}}
</button>
{{else}}
<button
class="js-navigate-to-thread rc-tags__tag rc-tags--no-icon no-replies"
data-rid={{roomId}}
>
{{_ "No_replies_yet" }}
</button>
{{/if}}
{{/with}}
</template>

@ -1,30 +0,0 @@
import { registerFieldTemplate } from '../../../message-attachments';
import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
Template.MessageCounter.helpers({
hasReplies() {
return Template.instance().data.field.count > 0;
},
replyCount() {
return Template.instance().data.field.count;
},
i18nKeyReply() {
return Template.instance().data.field.count > 1
? 'Replies'
: 'Reply';
},
});
const events = {
'click .js-navigate-to-thread'(event) {
event.preventDefault();
const [, { trid }] = this._arguments;
FlowRouter.goToRoomById(trid);
},
};
registerFieldTemplate('messageCounter', 'MessageCounter', events);

@ -1,42 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings';
Meteor.startup(() => {
settings.addGroup('Threading', function() {
// the channel for which threads are created if none is explicitly chosen
this.add('Thread_from_context_menu', 'button', {
group: 'Threading',
i18nLabel: 'Thread_from_context_menu',
type: 'select',
values: [
{ key: 'button', i18nLabel: 'Threading_context_menu_button' },
{ key: 'none', i18nLabel: 'Threading_context_menu_none' },
],
public: true,
});
});
settings.add('Accounts_Default_User_Preferences_sidebarShowThreads', true, {
group: 'Accounts',
section: 'Accounts_Default_User_Preferences',
type: 'boolean',
public: true,
i18nLabel: 'Threads_in_sidebar',
});
const globalQuery = {
_id: 'RetentionPolicy_Enabled',
value: true,
};
settings.add('RetentionPolicy_DoNotExcludeThreads', true, {
group: 'RetentionPolicy',
section: 'Global Policy',
type: 'boolean',
public: true,
i18nLabel: 'RetentionPolicy_DoNotExcludeThreads',
i18nDescription: 'RetentionPolicy_DoNotExcludeThreads_Description',
enableQuery: globalQuery,
});
});

@ -1,28 +0,0 @@
import { callbacks } from '../../../callbacks';
import { Messages, Rooms } from '../../../models';
import { deleteRoom } from '../../../lib';
/**
* We need to propagate the writing of new message in a thread to the linking
* system message
*/
callbacks.add('afterSaveMessage', function(message, { _id, prid } = {}) {
if (prid) {
Messages.refreshThreadMetadata({ rid: _id }, message);
}
return message;
}, callbacks.priority.LOW, 'PropagateThreadMetadata');
callbacks.add('afterDeleteMessage', function(message, { _id, prid } = {}) {
if (prid) {
Messages.refreshThreadMetadata({ rid: _id }, message);
}
if (message.trid) {
deleteRoom(message.trid);
}
return message;
}, callbacks.priority.LOW, 'PropagateThreadMetadata');
callbacks.add('afterDeleteRoom', function(rid) {
Rooms.find({ prid: rid }, { fields: { _id: 1 } }).forEach(({ _id }) => deleteRoom(_id));
}, 'DeleteThreadChain');

@ -1,14 +0,0 @@
import './config';
import './authorization';
import './permissions';
import './hooks/joinThreadOnMessage';
import './hooks/propagateThreadMetadata';
import './publications/threadParentAutocomplete';
import './publications/threadsOfRoom';
// Methods
import './methods/createThread';
// Lib
import '../lib/threadRoomType';

@ -1,162 +0,0 @@
/* UserRoles RoomRoles*/
// import { FlowRouter } from 'meteor/kadira:flow-router';
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
// import { getAvatarUrlFromUsername } from '../../../utils';
import { hasAtLeastOnePermission, canAccessRoom } from '../../../authorization';
import { Messages, Rooms } from '../../../models';
import { createRoom, addUserToRoom, sendMessage, attachMessage } from '../../../lib';
const fields = [
{
type: 'messageCounter',
count: 0,
},
{
type: 'lastMessageAge',
lm: null,
},
];
export const createThreadMessage = (rid, user, trid, msg, message_embedded) => {
const welcomeMessage = {
msg,
rid,
trid,
attachments: [{
fields,
}, message_embedded].filter((e) => e),
};
return Messages.createWithTypeRoomIdMessageAndUser('thread-created', trid, '', user, welcomeMessage);
};
export const mentionThreadMessage = (rid, user, msg, message_embedded) => {
const welcomeMessage = {
msg,
rid,
attachments: [message_embedded].filter((e) => e),
};
return Messages.createWithTypeRoomIdMessageAndUser('thread-created', rid, '', user, welcomeMessage);
};
const cloneMessage = ({ _id, ...msg }) => ({ ...msg });
export const create = ({ prid, pmid, t_name, reply, users }) => {
// if you set both, prid and pmid, and the rooms doesnt match... should throw an error)
let message = false;
if (pmid) {
message = Messages.findOne({ _id: pmid });
if (prid) {
if (prid !== message.rid) {
throw new Meteor.Error('error-invalid-arguments', { method: 'ThreadCreation' });
}
} else {
prid = message.rid;
}
}
if (!prid) {
throw new Meteor.Error('error-invalid-arguments', { method: 'ThreadCreation' });
}
const p_room = Rooms.findOne(prid);
if (p_room.prid) {
throw new Meteor.Error('error-nested-thread', 'Cannot create nested threads', { method: 'ThreadCreation' });
}
const user = Meteor.user();
if (!canAccessRoom(p_room, user)) {
throw new Meteor.Error('error-not-allowed', { method: 'ThreadCreation' });
}
if (pmid) {
const threadAlreadyExists = Rooms.findOne({
prid,
pmid,
}, {
fields: { _id: 1 },
});
if (threadAlreadyExists) { // do not allow multiple threads to the same message'\
addUserToRoom(threadAlreadyExists._id, user);
return threadAlreadyExists;
}
}
const name = Random.id();
// auto invite the replied message owner
const invitedUsers = message ? [message.u.username, ...users] : users;
// threads are always created as private groups
const thread = createRoom('p', name, user.username, [...new Set(invitedUsers)], false, {
fname: t_name,
description: message.msg, // TODO threads remove
topic: p_room.name, // TODO threads remove
prid,
});
if (pmid) {
const clonedMessage = cloneMessage(message);
Messages.update({
_id: message._id,
}, {
...clonedMessage,
attachments: [
{ fields },
...(message.attachments || []),
],
trid: thread._id,
});
mentionThreadMessage(thread._id, user, reply, attachMessage(message, p_room));
// check if the message is in the latest 10 messages sent to the room
// if not creates a new message saying about the thread creation
const lastMessageIds = Messages.findByRoomId(message.rid, {
sort: {
ts: -1,
},
limit: 15,
fields: {
_id: 1,
},
}).fetch();
if (!lastMessageIds.find((msg) => msg._id === message._id)) {
createThreadMessage(message.rid, user, thread._id, reply, attachMessage(message, p_room));
}
} else {
createThreadMessage(prid, user, thread._id, reply);
if (reply) {
sendMessage(user, { msg: reply }, thread);
}
}
return thread;
};
Meteor.methods({
/**
* Create thread by room or message
* @constructor
* @param {string} prid - Parent Room Id - The room id, optional if you send pmid.
* @param {string} pmid - Parent Message Id - Create the thread by a message, optional.
* @param {string} reply - The reply, optional
* @param {string} t_name - thread name
* @param {string[]} users - users to be added
*/
createThread({ prid, pmid, t_name, reply, users }) {
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ThreadCreation' });
}
if (!hasAtLeastOnePermission(uid, ['start-thread', 'start-thread-other-user'])) {
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a thread', { method: 'createThread' });
}
return create({ uid, prid, pmid, t_name, reply, users });
},
});

@ -1,31 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Messages } from '../../../models';
Meteor.publish('threadsOfRoom', function(rid, limit = 50) {
if (!this.userId) {
return this.ready();
}
const publication = this;
if (!Meteor.call('canAccessRoom', rid, this.userId)) {
return this.ready();
}
const cursorHandle = Messages.find({ rid, trid: { $exists: 1 } }, { sort: { ts: -1 }, limit }).observeChanges({
added(_id, record) {
return publication.added('rocketchat_threads_of_room', _id, record);
},
changed(_id, record) {
return publication.changed('rocketchat_threads_of_room', _id, record);
},
removed(_id) {
return publication.removed('rocketchat_threads_of_room', _id);
},
});
this.ready();
return this.onStop(function() {
return cursorHandle.stop();
});
});

@ -271,11 +271,11 @@
</div>
</div>
<div class="input-line double-col" id="sidebarShowThreads">
<label class="setting-label">{{_ "Threads_in_sidebar"}}</label>
<div class="input-line double-col" id="sidebarShowDiscussion">
<label class="setting-label">{{_ "Group_discussions"}}</label>
<div>
<label><input type="radio" name="sidebarShowThreads" value="true" checked="{{checked 'sidebarShowThreads' true}}"/> {{_ "True"}}</label>
<label><input type="radio" name="sidebarShowThreads" value="false" checked="{{checked 'sidebarShowThreads' false}}"/> {{_ "False"}}</label>
<label><input type="radio" name="sidebarShowDiscussion" value="true" checked="{{checked 'sidebarShowDiscussion' true}}"/> {{_ "True"}}</label>
<label><input type="radio" name="sidebarShowDiscussion" value="false" checked="{{checked 'sidebarShowDiscussion' false}}"/> {{_ "False"}}</label>
</div>
</div>
</div>

@ -167,7 +167,7 @@ Template.accountPreferences.onCreated(function() {
data.desktopNotifications = $('#desktopNotifications').find('select').val();
data.mobileNotifications = $('#mobileNotifications').find('select').val();
data.unreadAlert = JSON.parse($('#unreadAlert').find('input:checked').val());
data.sidebarShowThreads = JSON.parse($('#sidebarShowThreads').find('input:checked').val());
data.sidebarShowDiscussion = JSON.parse($('#sidebarShowDiscussion').find('input:checked').val());
data.notificationsSoundVolume = parseInt($('#notificationsSoundVolume').val());
data.roomCounterSidebar = JSON.parse($('#roomCounterSidebar').find('input:checked').val());
data.highlights = _.compact(_.map($('[name=highlights]').val().split(/,|\n/), function(e) {

@ -69,9 +69,9 @@
<span class="rc-checkbox__text rc-text__small">{{_ "Exclude_pinned"}}</span>
</label>
<label class="rc-checkbox">
<input type="checkbox" name="ignoreThreads" class="rc-checkbox__input">
<input type="checkbox" name="ignoreDiscussion" class="rc-checkbox__input">
{{> icon icon="check" block="rc-checkbox__icon"}}
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotExcludeThreads"}}</span>
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotExcludeDiscussion"}}</span>
</label>
<label class="rc-checkbox">
<input type="checkbox" name="filesOnly" class="rc-checkbox__input">

@ -22,7 +22,7 @@ const getRoomName = function() {
return t('conversation_with_s', roomTypes.getRoomName(room.t, room));
};
const purgeWorker = function(roomId, oldest, latest, inclusive, limit, excludePinned, ignoreThreads, filesOnly, fromUsers) {
const purgeWorker = function(roomId, oldest, latest, inclusive, limit, excludePinned, ignoreDiscussion, filesOnly, fromUsers) {
return call('cleanRoomHistory', {
roomId,
latest,
@ -30,7 +30,7 @@ const purgeWorker = function(roomId, oldest, latest, inclusive, limit, excludePi
inclusive,
limit,
excludePinned,
ignoreThreads,
ignoreDiscussion,
filesOnly,
fromUsers,
});
@ -140,7 +140,7 @@ Template.cleanHistory.onCreated(function() {
this.cleanHistoryExcludePinned = new ReactiveVar(false);
this.cleanHistoryFilesOnly = new ReactiveVar(false);
this.ignoreThreads = new ReactiveVar(false);
this.ignoreDiscussion = new ReactiveVar(false);
this.cleanHistoryBusy = new ReactiveVar(false);
@ -274,8 +274,8 @@ Template.cleanHistory.events({
'change [name=filesOnly]'(e, instance) {
instance.cleanHistoryFilesOnly.set(e.target.checked);
},
'change [name=ignoreThreads]'(e, instance) {
instance.ignoreThreads.set(e.target.checked);
'change [name=ignoreDiscussion]'(e, instance) {
instance.ignoreDiscussion.set(e.target.checked);
},
'click .js-prune'(e, instance) {
@ -299,7 +299,7 @@ Template.cleanHistory.events({
const metaCleanHistoryInclusive = instance.cleanHistoryInclusive.get();
const metaCleanHistoryExcludePinned = instance.cleanHistoryExcludePinned.get();
const metaCleanHistoryFilesOnly = instance.cleanHistoryFilesOnly.get();
const ignoreThreads = instance.ignoreThreads.get();
const ignoreDiscussion = instance.ignoreDiscussion.get();
let fromDate = new Date('0001-01-01T00:00:00Z');
let toDate = new Date('9999-12-31T23:59:59Z');
@ -318,7 +318,7 @@ Template.cleanHistory.events({
let count = 0;
let result;
do {
result = await purgeWorker(roomId, fromDate, toDate, metaCleanHistoryInclusive, limit, metaCleanHistoryExcludePinned, ignoreThreads, metaCleanHistoryFilesOnly, users);
result = await purgeWorker(roomId, fromDate, toDate, metaCleanHistoryInclusive, limit, metaCleanHistoryExcludePinned, ignoreDiscussion, metaCleanHistoryFilesOnly, users);
count += result;
} while (result === limit);

@ -58,7 +58,7 @@
{{#if isIgnored}}
<span class="toggle-hidden icon-right-dir" data-message="{{_id}}"> {{_ "Message_Ignored"}} </span>
{{/if}}
<div class="body color-primary-font-color {{system true}}" dir="auto" data-unread-text="{{_ "Unread_Messages"}}">
<div class="body {{bodyClass}}" dir="auto" data-unread-text="{{_ "Unread_Messages"}}">
{{#if isSnippet}}
<div class="snippet-name">{{_ "Snippet_name"}}: {{snippetName}}</div>
{{/if}}
@ -76,6 +76,23 @@
{{injectIndex . @index}} {{> messageAttachment}}
{{/each}}
</div>
{{#if drid}}
<div class="message-discussion">
{{#if $gt dcount 0}}
<button class="js-navigate-to-discussion rc-button rc-button--primary rc-button--small" data-rid={{roomId}}>
<!-- {{> icon icon="discussion"}} -->
<span class='reply-counter'>{{dcount}}</span>&nbsp;{{_ i18nKeyReply}}
</button>
{{else}}
<button class="js-navigate-to-discussion rc-button rc-button--primary rc-button--small no-replies" data-rid={{roomId}}>
{{_ "No_messages_yet" }}
</button>
{{/if}}
<span class="discussion-reply-lm">{{dlm}}</span>
</div>
{{/if}}
{{#with readReceipt}}
<div class="read-receipt {{readByEveryone}}">
{{> icon icon="check" }}

@ -68,6 +68,14 @@ async function renderPdfToCanvas(canvasId, pdfLink) {
}
Template.message.helpers({
i18nKeyReply() {
return this.dcount > 1
? 'messages'
: 'message';
},
dlm() {
return this.dlm && moment(this.dlm).format('LLL');
},
encodeURI(text) {
return encodeURI(text);
},
@ -177,6 +185,9 @@ Template.message.helpers({
body() {
return Template.instance().body;
},
bodyClass() {
return MessageTypes.isSystemMessage(this) ? 'color-info-font-color' : 'color-primary-font-color';
},
system(returnClass) {
if (MessageTypes.isSystemMessage(this)) {
if (returnClass) {

@ -317,7 +317,7 @@ Template.messageBox.events({
'click .cancel-reply'(event, instance) {
const input = instance.find('.js-input-message');
const messages = $(input).data('reply');
const messages = $(input).data('reply') || [];
const filtered = messages.filter((msg) => msg._id !== this._id);
$(input)

@ -40,7 +40,7 @@ Template.chatRoomItem.helpers({
};
roomData.username = roomData.username || roomData.name;
// hide icon for threads
// hide icon for discussions
if (this.prid) {
roomData.darken = true;
}

@ -23,7 +23,7 @@ Template.roomList.helpers({
'settings.preferences.sidebarSortby': 1,
'settings.preferences.sidebarShowFavorites': 1,
'settings.preferences.sidebarShowUnread': 1,
'settings.preferences.sidebarShowThreads': 1,
'settings.preferences.sidebarShowDiscussion': 1,
'services.tokenpass': 1,
},
});
@ -59,7 +59,7 @@ Template.roomList.helpers({
types = ['c', 'p', 'd'];
}
if (this.identifier === 'thread') {
if (this.identifier === 'discussion') {
types = ['c', 'p', 'd'];
query.prid = { $exists: true };
}
@ -74,8 +74,8 @@ Template.roomList.helpers({
query.tokens = { $exists: true };
}
// if we display threads as a separate group, we should hide them from the other lists
if (getUserPreference(user, 'sidebarShowThreads')) {
// if we display discussions as a separate group, we should hide them from the other lists
if (getUserPreference(user, 'sidebarShowDiscussion')) {
query.prid = { $exists: false };
}

@ -167,6 +167,29 @@ const toolbarButtons = (user) => [{
icon: 'edit-rounded',
condition: () => hasAtLeastOnePermission(['create-c', 'create-p']),
action: (e) => {
const createChannel = (e) => {
e.preventDefault();
modal.open({
title: t('Create_A_New_Channel'),
content: 'createChannel',
data: {
onCreate() {
modal.close();
},
},
modifier: 'modal',
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,
});
};
const discussionEnabled = settings.get('Discussion_enabled');
if (!discussionEnabled) {
return createChannel(e);
}
const config = {
columns: [
{
@ -176,37 +199,22 @@ const toolbarButtons = (user) => [{
{
icon: 'hashtag',
name: t('Channel'),
action: (e) => {
e.preventDefault();
modal.open({
title: t('Create_A_New_Channel'),
content: 'createChannel',
data: {
onCreate() {
modal.close();
},
},
modalClass: 'modal',
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,
});
},
action: createChannel,
},
{
icon: 'thread',
name: t('Thread'),
icon: 'discussion',
name: t('Discussion'),
action: (e) => {
e.preventDefault();
modal.open({
title: t('Threading_title'),
content: 'CreateThread',
title: t('Discussion_title'),
content: 'CreateDiscussion',
data: {
onCreate() {
modal.close();
},
},
modalClass: 'modal',
modifier: 'modal',
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,

@ -22,6 +22,15 @@
</ul>
<span class="rc-popover__divider"></span>
<ul class="rc-popover__list">
<li class="rc-popover__item {{bold 'sidebarShowDiscussion'}}">
<label class="rc-popover__label">
<input type="checkbox" name="sidebarShowDiscussion" class="hidden" checked="{{checked 'sidebarShowDiscussion'}}" />
<span class="rc-popover__icon">
{{> icon block="rc-popover__icon-element" icon='discussion' }}
</span>
<span class="rc-popover__item-text">{{_ "Group_discussions"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'sidebarGroupByType'}}">
<label class="rc-popover__label">
<input type="checkbox" name="sidebarGroupByType" class="hidden" checked="{{checked 'sidebarGroupByType'}}"/>

@ -6,6 +6,10 @@ import { settings } from '../../settings';
const checked = function(prop, field) {
const userId = Meteor.userId();
if (prop === 'sidebarShowDiscussion') {
return getUserPreference(userId, 'sidebarShowDiscussion');
}
if (prop === 'sidebarShowFavorites') {
return getUserPreference(userId, 'sidebarShowFavorites');
}

@ -18,13 +18,13 @@ import { mainReady } from './mainReady';
const maxRoomsOpen = parseInt(localStorage && localStorage.getItem('rc-maxRoomsOpen')) || 5 ;
const onDeleteMessageStream = (msg) => ChatMessage.remove({ _id: msg._id });
const onDeleteMessageBulkStream = ({ rid, ts, excludePinned, ignoreThreads, users }) => {
const onDeleteMessageBulkStream = ({ rid, ts, excludePinned, ignoreDiscussion, users }) => {
const query = { rid, ts };
if (excludePinned) {
query.pinned = { $ne: true };
}
if (ignoreThreads) {
query.trid = { $exists: false };
if (ignoreDiscussion) {
query.drid = { $exists: false };
}
if (users && users.length) {
query['u.username'] = { $in: users };

@ -1,6 +1,6 @@
<template name="rc_modal">
<div class="rc-modal-wrapper">
<dialog class="rc-modal rc-modal--{{modalClass}}" data-modal="modal">
<dialog class="rc-modal rc-modal--{{modifier}}" data-modal="modal">
{{#if template}}
{{> Template.dynamic template=template data=data}}
{{else}}
@ -51,10 +51,12 @@
</label>
{{/if}}
</section>
<footer class="rc-modal__footer {{#unless showFooter}}rc-modal__footer--empty{{/unless}}">
<input class="rc-button rc-button--nude js-close {{#unless showCancelButton}}rc-button--invisible{{/unless}}" type="submit" data-button="cancel" value="{{cancelButtonText}}">
<input style="background-color:{{confirmButtonColor}}" class="rc-button rc-button--primary js-confirm {{#unless showConfirmButton}}rc-button--invisible{{/unless}}" type="submit" data-button="create" value="{{confirmButtonText}}">
</footer>
{{# if showFooter }}
<footer class="rc-modal__footer {{#unless showFooter}}rc-modal__footer--empty{{/unless}}">
<input class="rc-button rc-button--nude js-close {{#unless showCancelButton}}rc-button--invisible{{/unless}}" type="submit" data-button="cancel" value="{{cancelButtonText}}">
<input style="background-color:{{confirmButtonColor}}" class="rc-button rc-button--primary js-confirm {{#unless showConfirmButton}}rc-button--invisible{{/unless}}" type="submit" data-button="create" value="{{confirmButtonText}}">
</footer>
{{/if}}
{{/if}}
</dialog>
</div>

@ -93,6 +93,11 @@ export const modal = {
};
Template.rc_modal.helpers({
showFooter() {
const { showCancelButton, showConfirmButton } = this;
return showCancelButton || showConfirmButton;
},
hasAction() {
return !!this.action;
},

@ -6,7 +6,7 @@
{{> burger}}
</div>
{{#if isThread}}
{{#if isDiscussion}}
<div class="rc-header__block rc-header__block--action js-open-parent-channel">
<span class="rc-header__first-icon">{{> icon block="rc-header__icon rc-header__icon" icon="back"}}</span>
</div>

@ -17,7 +17,7 @@ const isSubscribed = (_id) => ChatSubscription.find({ rid: _id }).count() > 0;
const favoritesEnabled = () => settings.get('Favorite_Rooms');
const isThread = ({ _id }) => {
const isDiscussion = ({ _id }) => {
const room = ChatRoom.findOne({ _id });
return !!(room && room.prid);
};
@ -36,8 +36,8 @@ Template.headerRoom.helpers({
return TabBar.getButtons();
},
isThread() {
return isThread(Template.instance().data);
isDiscussion() {
return isDiscussion(Template.instance().data);
},
isTranslated() {
@ -115,7 +115,7 @@ Template.headerRoom.helpers({
},
showToggleFavorite() {
return !isThread(Template.instance().data) && isSubscribed(this._id) && favoritesEnabled();
return !isDiscussion(Template.instance().data) && isSubscribed(this._id) && favoritesEnabled();
},
fixedHeight() {

@ -360,13 +360,13 @@ export const ChatMessages = class ChatMessages {
confirmDeleteMsg(message, done = function() {}) {
if (MessageTypes.isSystemMessage(message)) { return; }
const room = message.trid && Rooms.findOne({
_id: message.trid,
const room = message.drid && Rooms.findOne({
_id: message.drid,
prid: { $exists: true },
});
modal.open({
title: t('Are_you_sure'),
text: room ? t('The_message_is_a_thread_you_will_not_be_able_to_recover') : t('You_will_not_be_able_to_recover'),
text: room ? t('The_message_is_a_discussion_you_will_not_be_able_to_recover') : t('You_will_not_be_able_to_recover'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',

@ -856,6 +856,11 @@ Template.room.events({
Meteor.call('sendMessage', msgObject);
});
},
'click .js-navigate-to-discussion'(event) {
event.preventDefault();
const [, { drid }] = this._arguments;
FlowRouter.goToRoomById(drid);
},
});

@ -95,7 +95,7 @@ import '../app/version-check';
import '../app/search';
import '../app/chatpal-search';
import '../app/lazy-load';
import '../app/threading';
import '../app/discussion';
import '../app/mail-messages';
import '../app/utils';
import '../app/settings';

@ -39,4 +39,3 @@ import '../app/ui-vrecord/client/vrecord.css';
import '../app/videobridge/client/stylesheets/video.less';
import '../app/wordpress/client/wordpress-login-button.css';
import '../app/katex/katex.min.css';
import '../app/threading/client/public/stylesheets/threading.css';

@ -2012,9 +2012,9 @@
"New_encryption_password": "Neues Verschlüsselungs-Passwort",
"New_role": "Neue Rolle",
"New_Room_Notification": "Neuer-Raum-Benachrichtigung",
"New_thread": "Neuer Thread",
"New_thread_name": "Ein sinnvoller Name für den Thread-Raum",
"New_thread_first_message": "Üblicherweise beginnt ein Thread mit einer Frage, bspw. \"Wie lade ich ein Bild hoch?\"",
"New_discussion": "Neuer Diskussion",
"New_discussion_name": "Ein sinnvoller Name für den Diskussion-Raum",
"New_discussion_first_message": "Üblicherweise beginnt ein Diskussion mit einer Frage, bspw. \"Wie lade ich ein Bild hoch?\"",
"New_Trigger": "Neuer Trigger",
"New_version_available_(s)": "Neue Version verfügbar (%s)",
"New_videocall_request": "Neuer Video-Anruf",
@ -2025,6 +2025,7 @@
"No_channel_with_name_%s_was_found": "Es wurde kein Kanal mit dem Namen <strong>\"%s\"</strong> gefunden!",
"No_channels_yet": "bisher nirgendwo dabei",
"No_direct_messages_yet": "Sie haben bisher keine Konversationen gestartet",
"No_discussions_yet": "Keine Discussion vorhanden",
"No_Encryption": "Keine Verschlüsselung",
"No_group_with_name_%s_was_found": "Es wurde keine private Gruppe mit dem Namen <strong>\"%s\"</strong> gefunden!",
"No_groups_yet": "Sie sind kein Mitglied einer privaten Gruppe",
@ -2745,28 +2746,6 @@
"This_month": "Diesen Monat",
"This_room_has_been_archived_by__username_": "Dieser Raum wurde von __username__ archiviert",
"This_room_has_been_unarchived_by__username_": "Dieser Raum wurde von __username__ aus dem Archiv geholt",
"Thread_creation_on_home": "Threads von der Home-Seite aus anlegen",
"Thread_default_parent_Channel": "Standard-Kanal für neue Threads",
"Thread_from_context_menu": "Threads im Kontext-Menü",
"Thread_invitations_threshold_description": "Max. Anzahl der Benutzer, die automatisch zu einem öffentlichen Thread hinzugezogen werden",
"Thread_invitations_threshold": "Max. Anzahl automatisch einzuladender Benutzer",
"Thread_name": "Thread Name",
"Thread_slash_command_description": "Erstelle einen Thread zum aktuellen Kanal",
"Thread_slash_command_params": "Ihre Nachricht",
"Thread_start": "Thread starten",
"Thread_target_channel_description": "Wähle einen Kanal oder eine Gruppe aus, die zu Deinem Anliegen passt",
"Thread_target_channel_prefix": "Du erstellst einen Thread in",
"Thread_target_channel_suffix": "- wähle einen anderen übergeordneten Kanal aus",
"Thread_target_channel": "Übergeordneter Kanal oder Gruppe",
"thread-created": "Ich habe einen neuen Thread angelegt: \"__message__\"",
"thread-welcome": "Danke __username__, dass Du einen neuen Thread angelegt hast! Ich habe für Dich Mitglieder aus __parentChannel__ eingeladen. Tipp: mit \"@all\" kannst Du sie anstupsen, wenn sich länger niemand melden sollte ;)",
"thread": "Thread",
"Threading_context_menu_button": "Separater Button",
"Threading_context_menu_none": "Unsichtbar",
"Threading_description": "Erstelle einen Thread, um wichtigen Dingen mehr Raum zu geben. Dort kannst Du mit allen verfügbaren Mitgliedern schreiben, ohne andere zu stören. So sorgst Du für etwas mehr Ordnung in Eurem Chat.",
"Threading_first_message_title": "Deine Nachricht",
"Threads": "Threads",
"Threading_title": "Einen neuen Thread anlegen",
"This_week": "Diese Woche",
"Thursday": "Donnerstag",
"Time_in_seconds": "Zeit in Sekunden",

@ -1037,6 +1037,16 @@
"Display_offline_form": "Display Offline Form",
"Display_unread_counter": "Display number of unread messages",
"Displays_action_text": "Displays action text",
"Discussion_name": "Discussion name",
"Discussions": "Discussions",
"Discussion_start": "Start a discussion",
"Discussion_target_channel_description": "Select a channel which is related to what you want to ask",
"Discussion_target_channel_prefix": "You are creating a discussion in",
"Discussion_target_channel": "Parent channel or group",
"discussion-created": "__message__",
"Discussion_description": "Help keeping an overview about what's going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.",
"Discussion_first_message_title": "Your message",
"Discussion_title": "Create a new discussion",
"Dont_ask_me_again": "Don't ask me again!",
"Dont_ask_me_again_list": "Don't ask me again list",
"Do_not_display_unread_counter": "Do not display any counter of this channel",
@ -1422,6 +1432,7 @@
"Graphql_CORS": "GraphQL CORS",
"Graphql_Subscription_Port": "GraphQL Subscription Port",
"Group_by_Type": "Group by Type",
"Group_discussions": "Group discussions",
"Group_favorites": "Group favorites",
"Group_mentions_disabled_x_members": "Group mentions `@all` and `@here` have been disabled for rooms with more than __total__ members.",
"Group_mentions_only": "Group mentions only",
@ -1920,7 +1931,6 @@
"Mentions_default": "Mentions (default)",
"Mentions_only": "Mentions only",
"Merge_Channels": "Merge Channels",
"Message": "Message",
"Message_AllowBadWordsFilter": "Allow Message bad words filtering",
"Message_AllowDeleting": "Allow Message Deleting",
"Message_AllowDeleting_BlockDeleteInMinutes": "Block Message Deleting After (n) Minutes",
@ -1991,9 +2001,10 @@
"Message_VideoRecorderEnabled": "Video Recorder Enabled",
"Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.",
"Message_view_mode_info": "This changes the amount of space messages take up on screen.",
"messages": "Messages",
"message": "message",
"Message": "Message",
"messages": "messages",
"Messages": "Messages",
"Mmessages": "messages",
"Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Messages that are sent to the Incoming WebHook will be posted here.",
"Meta": "Meta",
"Meta_custom": "Custom Meta Tags",
@ -2041,6 +2052,9 @@
"New_Application": "New Application",
"New_Custom_Field": "New Custom Field",
"New_Department": "New Department",
"New_discussion": "New discussion",
"New_discussion_name": "A meaningful name for the discussion room",
"New_discussion_first_message": "Usually, a discussion starts with a question, like \"How do I upload a picture?\"",
"New_integration": "New integration",
"New_line_message_compose_input": "`%s` - New line in message compose input",
"New_logs": "New logs",
@ -2053,9 +2067,6 @@
"New_encryption_password": "New encryption password",
"New_role": "New role",
"New_Room_Notification": "New Room Notification",
"New_thread": "New thread",
"New_thread_name": "A meaningful name for the thread room",
"New_thread_first_message": "Usually, a thread starts with a question, like \"How do I upload a picture?\"",
"New_Trigger": "New Trigger",
"New_version_available_(s)": "New version available (%s)",
"New_videocall_request": "New Video Call Request",
@ -2075,13 +2086,12 @@
"No_messages_yet": "No messages yet",
"No_pages_yet_Try_hitting_Reload_Pages_button": "No pages yet. Try hitting \"Reload Pages\" button.",
"No_pinned_messages": "No pinned messages",
"No_replies_yet": "No replies yet",
"No_results_found": "No results found",
"No_results_found_for": "No results found for:",
"No_snippet_messages": "No snippet",
"No_starred_messages": "No starred messages",
"No_such_command": "No such command: `/__command__`",
"No_threads_yet": "No threads yet",
"No_discussions_yet": "No discussions yet",
"No_user_with_username_%s_was_found": "No user with username <strong>\"%s\"</strong> was found!",
"Nobody_available": "Nobody available",
"Node_version": "Node Version",
@ -2387,7 +2397,7 @@
"Retail": "Retail",
"Retention_setting_changed_successfully": "Retention policy setting changed successfully",
"RetentionPolicy": "Retention Policy",
"RetentionPolicy_DoNotExcludeThreads": "Do not exclude thread messages",
"RetentionPolicy_DoNotExcludeDiscussion": "Do not exclude discussion messages",
"RetentionPolicy_RoomWarning": "Messages older than __time__ are automatically pruned here",
"RetentionPolicy_RoomWarning_Unpinned": "Unpinned messages older than __time__ are automatically pruned here",
"RetentionPolicy_RoomWarning_FilesOnly": "Files older than __time__ are automatically pruned here (messages stay intact)",
@ -2720,7 +2730,7 @@
"The_emails_are_being_sent": "The emails are being sent.",
"The_field_is_required": "The field %s is required.",
"The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "The image resize will not work because we can not detect ImageMagick or GraphicsMagick installed on your server.",
"The_message_is_a_thread_you_will_not_be_able_to_recover": "The message is a thread you will not be able to recover the messages!",
"The_message_is_a_discussion_you_will_not_be_able_to_recover": "The message is a discussion you will not be able to recover the messages!",
"The_peer__peer__does_not_exist": "The peer <em>__peer__</em> does not exist.",
"The_redirectUri_is_required": "The redirectUri is required",
"The_server_will_restart_in_s_seconds": "The server will restart in %s seconds",
@ -2794,29 +2804,8 @@
"This_month": "This Month",
"This_room_has_been_archived_by__username_": "This room has been archived by __username__",
"This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__",
"Thread_creation_on_home": "Create threads on home screen",
"Thread_default_parent_Channel": "Default channel for new Threads",
"Thread_from_context_menu": "Threads in context-menu",
"Thread_invitations_threshold_description": "Max. count of users who are automatically being invited into a public thread",
"Thread_invitations_threshold": "Max. users to be automatically invited",
"Thread_name": "Thread name",
"Thread_slash_command_description": "Creates a thread for the current channel",
"Thread_slash_command_params": "your message",
"Thread_start": "Start a thread",
"Thread_target_channel_description": "Select a channel which is related to what you want to ask",
"Thread_target_channel_prefix": "You are creating a thread in",
"Thread_target_channel_suffix": "- select a different parent channel",
"Thread_target_channel": "Parent channel or group",
"thread-created": "Started a new thread: \"__message__\"",
"thread-welcome": "Thanks __username__ for creating a thread! I invited some members from __parentChannel__ who shall be able to help you. Hint: You can poke them with \"@all\", in case there's nothing happening for a longer time ;)",
"thread": "thread",
"Threading_context_menu_button": "Dedicated button",
"Threading_context_menu_none": "Invisible",
"Threading_description": "Help keeping an overview about what's going on! By creating a thread, a sub-channel of the one you selected is created and both are linked.",
"Threading_first_message_title": "Your message",
"Threads_in_sidebar": "Show threads category on sidebar",
"Threads": "Threads",
"Threading_title": "Create a new thread",
"This_week": "This Week",
"Thursday": "Thursday",
"Time_in_seconds": "Time in seconds",
@ -3177,4 +3166,4 @@
"Your_question": "Your question",
"Your_server_link": "Your server link",
"Your_workspace_is_ready": "Your workspace is ready to use 🎉"
}
}

@ -89,7 +89,9 @@
*/
--button-square-size: 36px;
--button-padding: 0.782rem;
--button-padding-small: 0.5rem;
--button-padding-small: 0 0.5rem;
--button-height-small: 28px;
--button-text-size-small: 13px;
--button-text-size: var(--input-font-size);
--button-border-width: var(--border);
--button-border-radius: var(--border-radius);

@ -83,6 +83,9 @@
<symbol id="icon-discover" viewBox="0 0 20 20" fill="currentColor">
<path d="M6.40211645,13.5 L4.36976532,13.5 C5.08834307,14.6534694 6.15151935,15.5700421 7.41528581,16.1057095 C6.99352323,15.3908207 6.64680853,14.5042677 6.40211645,13.5 Z M6.12601749,12 C6.043753,11.3607602 6,10.6905991 6,10 C6,9.4873059 6.02411418,8.98587614 6.07019789,8.5 L3.54187189,8.5 C3.43036499,8.98199332 3.37142857,9.4841153 3.37142857,10 C3.37142857,10.697025 3.47901938,11.3689256 3.67849919,12 L6.12601749,12 Z M6.29075852,7 C6.53582402,5.7891713 6.92480779,4.72565214 7.41528581,3.89429054 C5.97389689,4.50524604 4.79344659,5.61170376 4.08759864,7 L6.29075852,7 Z M13.7092415,7 L15.9124014,7 C15.2065534,5.61170376 14.0261031,4.50524604 12.5847142,3.89429054 C13.0751922,4.72565214 13.464176,5.7891713 13.7092415,7 Z M13.9298021,8.5 C13.9758858,8.98587614 14,9.4873059 14,10 C14,10.6905991 13.956247,11.3607602 13.8739825,12 L16.3215008,12 C16.5209806,11.3689256 16.6285714,10.697025 16.6285714,10 C16.6285714,9.4841153 16.569635,8.98199332 16.4581281,8.5 L13.9298021,8.5 Z M13.5978835,13.5 C13.3531915,14.5042677 13.0064768,15.3908207 12.5847142,16.1057095 C13.8484806,15.5700421 14.9116569,14.6534694 15.6302347,13.5 L13.5978835,13.5 Z M7.87449573,13.5 C8.31545266,15.3608884 9.10249541,16.6 10,16.6 C10.8975046,16.6 11.6845473,15.3608884 12.1255043,13.5 L7.87449573,13.5 Z M7.61266,12 L12.38734,12 C12.4605549,11.3840508 12.5,10.7290943 12.5,10.05 C12.5,9.51599804 12.4756096,8.99692139 12.4295883,8.5 L7.57041167,8.5 C7.5243904,8.99692139 7.5,9.51599804 7.5,10.05 C7.5,10.7290943 7.53944513,11.3840508 7.61266,12 Z M7.7870057,7 L12.2129943,7 C11.7942682,4.91878844 10.9603738,3.5 10,3.5 C9.03962624,3.5 8.20573179,4.91878844 7.7870057,7 Z M10,18 C5.58217143,18 2,14.4178286 2,10 C2,5.58125714 5.58217143,2 10,2 C14.4187429,2 18,5.58125714 18,10 C18,14.4178286 14.4187429,18 10,18 Z"/>
</symbol>
<symbol version="1.1" id="icon-discussion" viewBox="0 0 20 20 " fill="currentColor">
<path d="M3.36312881,15.7593532 C3.32888874,15.7771726 3.29959994,15.7925621 3.27370934,15.8064174 C3.73031444,16.1917386 4.92393678,16.4549204 7.10274479,16.4476544 L7.21881505,16.4476539 C8.55574002,16.4476543 9.6998097,16.021859 10.6510241,15.170268 L12.188241,15.2712054 C11.5513943,15.8887667 11.2142468,16.2130071 11.1767984,16.2439265 C10.1491488,17.09241 8.76567843,17.5637907 7.21881505,17.5637907 L7.10806092,17.5644215 L6.97721254,17.5644231 C4.30241208,17.5644231 2.62246666,17.1951018 2.10341288,16.0930165 C1.82161029,15.4951149 2.13151102,15.1455492 2.84114675,14.7722012 C3.31318797,14.5227311 3.32172914,14.5103228 3.24657386,14.3473099 C3.20302404,14.2436958 3.16526615,14.1604157 3.07972552,13.9763174 C2.74839464,13.2684751 2.62592155,12.9023078 2.62592155,12.4061039 C2.62592155,11.562445 2.83045155,10.7486059 3.21214892,10.0211892 C3.42204019,9.66099218 3.78721597,9.06536542 4.270822,8.82818513 C4.270822,8.80275057 4.53792399,10.0211892 4.53792399,10 C4.45304843,10.1183326 4.36401443,10.2496015 4.270822,10.3938065 C3.78721597,11.0991197 3.7445882,11.6631394 3.7445882,12.4061039 C3.7445882,12.6985231 3.82924269,12.9394008 4.09330148,13.5047226 C4.19543836,13.7251753 4.22995648,13.801243 4.270822,13.8994264 C4.58782821,14.5810467 4.36137367,15.1036933 3.78721597,15.5080098 C3.65916127,15.5981847 3.55361796,15.6588423 3.36312881,15.7593532 Z M16.5696621,12.3226926 C16.5350205,12.3039671 16.4951623,12.2829692 16.4468239,12.2578135 C16.2265134,12.1415693 16.1048536,12.0716493 15.9574055,11.9678178 C15.2986909,11.5039571 15.0399564,10.9068098 15.4049351,10.1218159 C15.4504792,10.0121781 15.4903754,9.92425877 15.608285,9.66976084 C15.914896,9.01333804 16.0132801,8.73339397 16.0132801,8.39198996 C16.0132801,5.80517045 13.90418,3.70028722 11.3120655,3.70028722 C8.71995111,3.70028722 6.61085101,5.80517045 6.61085101,8.39198996 C6.61085101,11.1096858 8.86296092,13.082961 11.9786225,13.082961 L12.112848,13.0829616 C14.6633683,13.0914671 16.0501708,12.7797435 16.5696621,12.3226926 Z M16.5612822,10.6280948 C16.4685253,10.8294269 16.4833349,10.8509584 17.0358533,11.1429611 C17.8499187,11.5712649 18.2032866,11.969896 17.8824254,12.6506683 C17.2857779,13.9175051 15.3458564,14.3439799 12.2581228,14.3439799 L12.1073975,14.3439799 L11.9780624,14.3432467 C8.14202747,14.3432482 5.34836876,11.8401459 5.34836876,8.39198996 C5.34836876,5.10955211 8.0234498,2.44 11.3120655,2.44 C14.6008168,2.44 17.276494,5.10968724 17.276494,8.39198996 C17.276494,8.96303774 17.1353362,9.38506819 16.7528285,10.2022436 C16.6539056,10.4151439 16.6101724,10.5116034 16.5612822,10.6280948 Z" id="path-1"></path>
</symbol>
<symbol id="icon-download" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.0661854,13.8691048 L13.0661854,13.8691048 C13.3204823,14.1197896 13.3234104,14.5291581 13.0727256,14.783455 C13.0705612,14.7856506 13.0683811,14.7878308 13.0661854,14.7899952 L10.6543517,17.1675709 C10.2650167,17.5513757 9.63962467,17.5513757 9.25028971,17.1675709 L6.83845596,14.7899952 C6.58415908,14.5393104 6.58123091,14.1299419 6.83191572,13.875645 C6.83408017,13.8734494 6.83626031,13.8712692 6.83845596,13.8691048 L6.83845596,13.8691048 C7.09749077,13.6137493 7.51358056,13.6137493 7.77261537,13.8691048 L9.25137581,15.3268606 L9.25137581,10.0339893 C9.25137581,9.66917735 9.54711432,9.37343883 9.91192627,9.37343883 L9.91192627,9.37343883 C10.2767382,9.37343883 10.5724767,9.66917735 10.5724767,10.0339893 L10.5724767,15.4065019 L12.132026,13.8691048 C12.3910608,13.6137493 12.8071506,13.6137493 13.0661854,13.8691048 Z M14.9987456,5.44238699 C16.7017593,5.72193582 18,7.18117469 18,8.93932691 C18,10.897313 16.3898681,12.4845743 14.4036697,12.4845743 L12.8484335,12.4845743 C12.4888034,12.4845743 12.1972656,12.1930365 12.1972656,11.8334064 L12.1972656,11.8334064 C12.1972656,11.4737763 12.4888034,11.1822385 12.8484335,11.1822385 L14.4036697,11.1822385 C15.6602442,11.1822385 16.6788991,10.1780528 16.6788991,8.93932691 C16.6788991,7.70060103 15.6602442,6.69641529 14.4036697,6.69641529 L13.7431193,6.69641529 L13.7431193,6.0452474 C13.7431193,5.20611051 13.0530627,4.52585566 12.2018349,4.52585566 C11.8271022,4.52585566 11.4747517,4.6573116 11.1968202,4.89324889 L10.6727099,5.33816841 L10.2459585,4.80158937 C9.74752164,4.17487601 8.98815837,3.80233578 8.16513761,3.80233578 C6.70588984,3.80233578 5.52293578,4.96848696 5.52293578,6.40700734 C5.52293578,6.70702583 5.574134,6.99923245 5.67311153,7.27528801 L5.98438594,8.14345504 L4.86238532,8.14345504 C4.01115745,8.14345504 3.32110092,8.8237099 3.32110092,9.66284679 C3.32110092,10.5019837 4.01115745,11.1822385 4.86238532,11.1822385 L6.77545553,11.1822385 C7.13508563,11.1822385 7.42662342,11.4737763 7.42662342,11.8334064 L7.42662342,11.8334064 C7.42662342,12.1930365 7.13508563,12.4845743 6.77545553,12.4845743 L4.86238532,12.4845743 C3.28153356,12.4845743 2,11.2212439 2,9.66284679 C2,8.31713817 2.95559925,7.19145146 4.23434884,6.90925214 C4.21276266,6.74369761 4.20183486,6.57602338 4.20183486,6.40700734 C4.20183486,4.24922677 5.97626595,2.5 8.16513761,2.5 C9.18633879,2.5 10.1442246,2.88377985 10.8671476,3.5484842 C11.2727861,3.3374289 11.7287839,3.22351988 12.2018349,3.22351988 C13.5727949,3.22351988 14.718642,4.17365257 14.9987456,5.44238699 Z"/>
</symbol>

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 202 KiB

@ -102,7 +102,7 @@ import '../app/blockstack';
import '../app/version-check';
import '../app/search';
import '../app/chatpal-search';
import '../app/threading';
import '../app/discussion';
import '../app/bigbluebutton';
import '../app/mail-messages';
import '../app/utils';

@ -136,4 +136,5 @@ import './v135';
import './v136';
import './v137';
import './v138';
import './v139';
import './xrun';

@ -0,0 +1,118 @@
import { Migrations } from '../../../app/migrations/server';
import { Messages, Permissions, Rooms, Settings } from '../../../app/models/server';
const getField = (msg, fieldType, fieldName) => {
if (!msg.attachments) {
return;
}
const [attachments] = msg.attachments;
if (!attachments) {
return;
}
if (!attachments.fields) {
return;
}
const field = attachments.fields.find(({ type }) => fieldType === type);
return field && field[fieldName];
};
Migrations.add({
version: 139,
up() {
Messages.find({ trid: { $exists: true } }).forEach((msg) => {
const dlm = getField(msg, 'lastMessageAge', 'lm');
const dcount = getField(msg, 'messageCounter', 'count');
if (dlm === undefined || dcount === undefined) {
return;
}
const update = {
$set: {
t: 'discussion-created',
dlm,
dcount,
},
$unset: {
attachments: 1,
},
};
if (msg.t) {
const room = Rooms.findOne({ _id: msg.trid }, { fields: { fname: 1 } });
if (room) {
update.$set.msg = room.fname;
}
}
Messages.update({ _id: msg._id }, update);
});
Messages.update({ t: 'thread-created' }, {
$set: {
t: 'discussion-created',
},
}, { multi: true });
Messages.update({ trid: { $exists: true } }, {
$rename: {
trid: 'drid',
},
}, { multi: true });
const settingEnabled = Settings.findOne({ _id: 'Thread_from_context_menu' });
const settingRetention = Settings.findOne({ _id: 'RetentionPolicy_DoNotExcludeThreads' });
const settingSidebar = Settings.findOne({ _id: 'Accounts_Default_User_Preferences_sidebarShowThreads' });
if (settingEnabled) {
Settings.upsert({
_id: 'Discussion_enabled',
}, {
$set: {
value: settingEnabled.value !== 'none',
},
});
}
if (settingRetention) {
Settings.upsert({
_id: 'RetentionPolicy_DoNotExcludeDiscussion',
}, {
$set: {
value: settingRetention.value,
},
});
}
if (settingSidebar) {
Settings.upsert({
_id: 'Accounts_Default_User_Preferences_sidebarShowDiscussion',
}, {
$set: {
value: settingSidebar.value,
},
});
}
Settings.remove({
_id: {
$in: ['Thread_from_context_menu', 'Accounts_Default_User_Preferences_sidebarShowThreads', 'RetentionPolicy_DoNotExcludeThreads'],
},
});
Permissions.find({
_id: { $in: ['start-thread', 'start-thread-other-user'] },
}).forEach((perm) => {
Permissions.remove({ _id: perm._id });
const newId = perm._id === 'start-thread' ? 'start-discussion' : 'start-discussion-other-user';
delete perm._id;
Permissions.upsert({
_id: newId,
}, {
$set: perm,
});
});
},
});

@ -109,7 +109,7 @@ describe('miscellaneous', function() {
const allUserPreferencesKeys = ['enableAutoAway', 'idleTimeLimit', 'desktopNotificationDuration', 'audioNotifications',
'desktopNotifications', 'mobileNotifications', 'unreadAlert', 'useEmojis', 'convertAsciiEmoji', 'autoImageLoad',
'saveMobileBandwidth', 'collapseMediaByDefault', 'hideUsernames', 'hideRoles', 'hideFlexTab', 'hideAvatars',
'sidebarViewMode', 'sidebarHideAvatar', 'sidebarShowUnread', 'sidebarShowThreads', 'sidebarShowFavorites', 'sidebarGroupByType',
'sidebarViewMode', 'sidebarHideAvatar', 'sidebarShowUnread', 'sidebarShowDiscussion', 'sidebarShowFavorites', 'sidebarGroupByType',
'sendOnEnter', 'messageViewMode', 'emailNotificationMode', 'roomCounterSidebar', 'newRoomNotification', 'newMessageNotification',
'muteFocusedConversations', 'notificationsSoundVolume'];
expect(res.body).to.have.property('success', true);

@ -5,14 +5,14 @@ quotes, prefer-template, no-undef, no-unused-vars*/
import mainContent from '../../pageobjects/main-content.page';
import sideNav from '../../pageobjects/side-nav.page';
import { sendEscape } from '../../pageobjects/keyboard';
import { threading } from '../../pageobjects/threading.page';
import { discussion } from '../../pageobjects/discussion.page';
import { username, email, password } from '../../data/user.js';
import { checkIfUserIsValid } from '../../data/checks';
const parentChannelName = 'unit-testing';
const threadName = 'Lorem ipsum dolor sit amet';
const discussionName = 'Lorem ipsum dolor sit amet';
const message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
describe('[Threading]', function () {
describe('[Discussion]', function () {
before(function () {
checkIfUserIsValid(username, email, password);
@ -30,8 +30,8 @@ describe('[Threading]', function () {
});
describe('via creation screen', function() {
it('Create a thread', function () {
threading.createThread(parentChannelName, threadName, message);
it('Create a discussion', function () {
discussion.createDiscussion(parentChannelName, discussionName, message);
});
});
@ -41,9 +41,9 @@ describe('[Threading]', function () {
mainContent.sendMessage(message);
});
it('it should show a dialog for starting a thread', () => {
it('it should show a dialog for starting a discussion', () => {
mainContent.openMessageActionMenu();
threading.startThreadContextItem.click();
discussion.startDiscussionContextItem.click();
});
it('it should have create a new room', function () {
@ -57,7 +57,7 @@ describe('[Threading]', function () {
after(function () {
it('remove parent channel', () => {
threading.deleteRoom(parentChannelName);
discussion.deleteRoom(parentChannelName);
});
});

@ -4,10 +4,10 @@ import sideNav from './side-nav.page';
import flexTab from './flex-tab.page';
import global from './global';
class Threading extends Page {
class Discussion extends Page {
// Sidebar - this should actually be part of the sidebar-file - leaving it here for mergability
get newThreadButton() {
return browser.element('.menu-nav .js-create-thread');
get newDiscussionButton() {
return browser.element('.menu-nav .js-create-discussion');
}
// Global - this should actually be part of the global-file - leaving it here for mergability
@ -22,39 +22,39 @@ class Threading extends Page {
}
// Action Menu
get startThreadContextItem() { return browser.element('[data-id="start-thread"][data-type="message-action"]'); }
get startDiscussionContextItem() { return browser.element('[data-id="start-discussion"][data-type="message-action"]'); }
// Modal
get createThreadModal() {
return browser.element('#create-thread');
get createDiscussionModal() {
return browser.element('#create-discussion');
}
get threadName() {
return browser.element('#create-thread #thread_name');
get discussionName() {
return browser.element('#create-discussion #discussion_name');
}
get threadMessage() {
return browser.element('#create-thread #thread_message');
get discussionMessage() {
return browser.element('#create-discussion #discussion_message');
}
get parentChannelName() {
return browser.element('#create-thread #parentChannel');
return browser.element('#create-discussion #parentChannel');
}
get saveThreadButton() {
return browser.element('.js-save-thread');
get saveDiscussionButton() {
return browser.element('.js-save-discussion');
}
// Sequences
createThread(parentChannelName, name, message) {
createDiscussion(parentChannelName, name, message) {
sideNav.newChannelBtnToolbar.waitForVisible(1000);
sideNav.newChannelBtnToolbar.click();
sideNav.newThreadBtn.waitForVisible(1000);
sideNav.newThreadBtn.click();
this.createThreadModal.waitForVisible(1000);
this.threadName.setValue(name);
this.threadMessage.setValue(message);
sideNav.newDiscussionBtn.waitForVisible(1000);
sideNav.newDiscussionBtn.click();
this.createDiscussionModal.waitForVisible(1000);
this.discussionName.setValue(name);
this.discussionMessage.setValue(message);
this.parentChannelName.waitForVisible(1000);
this.parentChannelName.setValue(parentChannelName);
@ -65,13 +65,13 @@ class Threading extends Page {
list.element('.rc-popup-list__item').click();
browser.waitUntil(function() {
return browser.isEnabled('.js-save-thread');
return browser.isEnabled('.js-save-discussion');
}, 5000);
this.saveThreadButton.click();
this.saveDiscussionButton.click();
}
}
const threading = new Threading();
const discussion = new Discussion();
export { threading };
export { discussion };

@ -32,7 +32,7 @@ class SideNav extends Page {
get newChannelBtnToolbar() { return browser.element('.sidebar__toolbar-button-icon--edit-rounded'); }
get newChannelBtn() { return browser.element('.rc-popover__icon-element--hashtag'); }
get newThreadBtn() { return browser.element('.rc-popover__icon-element--thread'); }
get newDiscussionBtn() { return browser.element('.rc-popover__icon-element--discussion'); }
get newChannelIcon() { return browser.element('.toolbar__icon.toolbar__search-create-channel'); }

Loading…
Cancel
Save