Rename Threads to Discussion (#13782)
parent
ceb653921b
commit
be18e1898d
@ -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,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 }); |
||||
}, |
||||
}); |
@ -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(); |
||||
}); |
||||
}); |
@ -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> {{_ 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(); |
||||
}); |
||||
}); |
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 202 KiB |
@ -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, |
||||
}); |
||||
}); |
||||
}, |
||||
}); |
Loading…
Reference in new issue