diff --git a/common/locales/en/groups.json b/common/locales/en/groups.json index c5c2cf8183..6ade797459 100644 --- a/common/locales/en/groups.json +++ b/common/locales/en/groups.json @@ -96,5 +96,9 @@ "abuseReported": "Thank you for reporting this violation. The moderators have been notified.", "abuseAlreadyReported": "You have already reported this message.", "needsText": "Please type a message.", - "needsTextPlaceholder": "Type your message here." + "needsTextPlaceholder": "Type your message here.", + "copyMessageAsToDo": "Copy message as To-Do", + "messageAddedAsToDo": "Message copied as To-Do.", + "messageWroteIn": "<%= user %> wrote in <%= group %>", + "msgPreviewHeading": "Message Preview" } diff --git a/test/spec/groupCtrlSpec.js b/test/spec/groupCtrlSpec.js index bd633b914c..21a43affb0 100644 --- a/test/spec/groupCtrlSpec.js +++ b/test/spec/groupCtrlSpec.js @@ -68,6 +68,76 @@ describe('Groups Controller', function() { }); }); +describe("Chat Controller", function() { + var scope, ctrl, user, $rootScope, $controller; + + beforeEach(function() { + module(function($provide) { + $provide.value('User', {}); + }); + + inject(function(_$rootScope_, _$controller_){ + user = specHelper.newUser(); + user._id = "unique-user-id"; + $rootScope = _$rootScope_; + + scope = _$rootScope_.$new(); + + $controller = _$controller_; + + // Load RootCtrl to ensure shared behaviors are loaded + $controller('RootCtrl', {$scope: scope, User: {user: user}}); + + ctrl = $controller('ChatCtrl', {$scope: scope}); + }); + }); + + describe('copyToDo', function() { + it('when copying a user message it opens modal with information from message', function() { + scope.group = { + name: "Princess Bride" + }; + + var modalSpy = sinon.spy($rootScope, "openModal"); + var message = { + uuid: 'the-dread-pirate-roberts', + user: 'Wesley', + text: 'As you wish' + }; + + scope.copyToDo(message); + + modalSpy.should.have.been.calledOnce; + + modalSpy.should.have.been.calledWith('copyChatToDo', sinon.match(function(callArgToMatch){ + return callArgToMatch.controller == 'CopyMessageModalCtrl' + && callArgToMatch.scope.text == message.text + })); + }); + + it('when copying a system message it opens modal with information from message', function() { + scope.group = { + name: "Princess Bride" + }; + + var modalSpy = sinon.spy($rootScope, "openModal"); + var message = { + uuid: 'system', + text: 'Wesley attacked the ROUS in the Fire Swamp' + }; + + scope.copyToDo(message); + + modalSpy.should.have.been.calledOnce; + + modalSpy.should.have.been.calledWith('copyChatToDo', sinon.match(function(callArgToMatch){ + return callArgToMatch.controller == 'CopyMessageModalCtrl' + && callArgToMatch.scope.text == message.text + })); + }); + }); +}); + describe("Autocomplete controller", function() { var scope, ctrl, user, $rootScope, $controller; @@ -172,3 +242,57 @@ describe("Autocomplete controller", function() { }); }); }); + +describe("CopyMessageModal controller", function() { + var scope, ctrl, user, Notification, $rootScope, $controller; + + beforeEach(function() { + module(function($provide) { + $provide.value('User', {}); + }); + + inject(function($rootScope, _$controller_, _Notification_){ + user = specHelper.newUser(); + user._id = "unique-user-id"; + user.ops = { + addTask: sinon.spy() + }; + + scope = $rootScope.$new(); + scope.$close = sinon.spy(); + + $controller = _$controller_; + + // Load RootCtrl to ensure shared behaviors are loaded + $controller('RootCtrl', {$scope: scope, User: {user: user}}); + + ctrl = $controller('CopyMessageModalCtrl', {$scope: scope, User: {user: user}}); + + Notification = _Notification_; + Notification.text = sinon.spy(); + }); + }); + + describe("saveTodo", function() { + it('saves todo', function() { + + scope.text = "A Tavern msg"; + scope.notes = "Some notes"; + var payload = { + body: { + text: scope.text, + type: 'todo', + notes: scope.notes + } + }; + + scope.saveTodo(); + + user.ops.addTask.should.have.been.calledOnce; + user.ops.addTask.should.have.been.calledWith(payload); + Notification.text.should.have.been.calledOnce; + Notification.text.should.have.been.calledWith(window.env.t('messageAddedAsToDo')); + scope.$close.should.have.been.calledOnce; + }); + }); +}); diff --git a/website/public/css/index.styl b/website/public/css/index.styl index b7fd653a05..b93b9f77fd 100644 --- a/website/public/css/index.styl +++ b/website/public/css/index.styl @@ -187,3 +187,6 @@ a.label color: #fff !important .line-through text-decoration line-through + +.markdown-preview markdown code + white-space inherit \ No newline at end of file diff --git a/website/public/css/tasks.styl b/website/public/css/tasks.styl index 5b1bbdc348..59ea60e0e7 100644 --- a/website/public/css/tasks.styl +++ b/website/public/css/tasks.styl @@ -142,6 +142,14 @@ for $stage in $stages padding: 0 font-weight: 300 +.task-column.preview + padding: 0 + background: transparent + border: 0; + + .task:hover + cursor: auto + // 50% width columns with scrollbars for tablets @media (min-width: 768px) and (max-width: 970px) .task-column diff --git a/website/public/js/controllers/groupsCtrl.js b/website/public/js/controllers/groupsCtrl.js index 26a4196a97..f8a099d435 100644 --- a/website/public/js/controllers/groupsCtrl.js +++ b/website/public/js/controllers/groupsCtrl.js @@ -362,7 +362,25 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' }); }); } - } + }; + + $scope.copyToDo = function(message) { + var taskNotes = env.t("messageWroteIn", { + user: message.uuid == 'system' + ? 'system' + : '[' + message.user + '](' + env.BASE_URL + '/static/front/#?memberId=' + message.uuid + ')', + group: '[' + $scope.group.name + '](' + window.location.href + ')' + }); + + var newScope = $scope.$new(); + newScope.text = message.text; + newScope.notes = taskNotes; + + $rootScope.openModal('copyChatToDo',{ + controller:'CopyMessageModalCtrl', + scope: newScope + }); + }; $scope.sync = function(group){ group.$get(); @@ -567,3 +585,20 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } } ]) + + .controller("CopyMessageModalCtrl", ['$scope', 'User', 'Notification', + function($scope, User, Notification){ + $scope.saveTodo = function() { + var newTask = { + text: $scope.text, + type: 'todo', + notes: $scope.notes + }; + + User.user.ops.addTask({body:newTask}); + Notification.text(window.env.t('messageAddedAsToDo')); + + $scope.$close(); + } + } + ]); diff --git a/website/views/options/social/chat-message.jade b/website/views/options/social/chat-message.jade index 48c6d85e7f..944088faa4 100644 --- a/website/views/options/social/chat-message.jade +++ b/website/views/options/social/chat-message.jade @@ -31,6 +31,9 @@ mixin chatMessages(inbox) |     a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")') span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""') + |     + a(ng-click="copyToDo(message)") + span.glyphicon.glyphicon-share(tooltip=env.t('copyMessageAsToDo')) span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""') a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') diff --git a/website/views/options/social/index.jade b/website/views/options/social/index.jade index 5cbea6e7c0..a965b5d1d3 100644 --- a/website/views/options/social/index.jade +++ b/website/views/options/social/index.jade @@ -113,3 +113,33 @@ script(type='text/ng-template', id='partials/options.social.html') .tab-content .tab-pane.active div(ui-view) + +script(type='text/ng-template', id='modals/copyChatToDo.html') + .modal-header + h4=env.t('copyMessageAsToDo') + .modal-body + .form-group + input.form-control(type='text',ng-model='text', ng-model-options="{debounce: 1000}") + .form-group + textarea.form-control(rows='5',ng-model='notes', ng-model-options="{debounce: 1000}", focus-me) + + hr + + div.task-column.preview + div(ng-init='popoverOpen = false', class='task todo uncompleted color-neutral', popover-trigger='mouseenter', data-popover-html="{{popoverOpen ? '' : notes | markdown}}", popover-placement="top") + // right-hand side control buttons + .task-meta-controls + // Icons only available if you own the tasks (aka, hidden from challenge stats) + span(ng-if='!obj._locked') + // notes + span.task-notes(ng-show='notes', ng-click='popoverOpen = !popoverOpen', popover-trigger='click', data-popover-html="{{notes | markdown}}", popover-placement="top") + span.glyphicon.glyphicon-comment + |   + + // main content + div.task-text + markdown(text='text',target='_blank') + + .modal-footer + button.btn.btn-default(ng-click='$close()')=env.t('close') + button.btn.btn-primary(ng-click='saveTodo()')=env.t('submit') \ No newline at end of file diff --git a/website/views/shared/mixins.jade b/website/views/shared/mixins.jade index f2c37b9b56..c4dde11e38 100644 --- a/website/views/shared/mixins.jade +++ b/website/views/shared/mixins.jade @@ -11,3 +11,9 @@ mixin aLink(url, label) a(href="", ng-click="externalLink('#{url}')")= label else a(href='#{url}', target='_blank')= label + +mixin previewMarkdown(text) + .panel.panel-warning + .panel-heading=env.t('msgPreviewHeading') + .panel-body.markdown-preview + markdown(text='#{text}') \ No newline at end of file diff --git a/website/views/shared/tasks/task.jade b/website/views/shared/tasks/task.jade index 9a5644094f..05661a939e 100644 --- a/website/views/shared/tasks/task.jade +++ b/website/views/shared/tasks/task.jade @@ -1,4 +1,4 @@ -li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"] | conditionalOrderBy: list.view=="dated":"date"', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', ng-show='shouldShow(task, list, user.preferences)') +li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"] | conditionalOrderBy: list.view=="dated":"date"', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', popover-trigger='mouseenter', data-popover-html="{{task.popoverOpen ? '' : task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', ng-show='shouldShow(task, list, user.preferences)') // right-hand side control buttons .task-meta-controls @@ -53,7 +53,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s span.glyphicon.glyphicon-signal |   // notes - span.task-notes(ng-show='task.notes && !task._editing') + span.task-notes(ng-show='task.notes && !task._editing', ng-click='task.popoverOpen = !task.popoverOpen', popover-trigger='click', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}') span.glyphicon.glyphicon-comment |