diff options
Diffstat (limited to 'client/components/main')
-rw-r--r-- | client/components/main/events.js | 8 | ||||
-rw-r--r-- | client/components/main/header.jade | 40 | ||||
-rw-r--r-- | client/components/main/header.js | 10 | ||||
-rw-r--r-- | client/components/main/header.styl | 266 | ||||
-rw-r--r-- | client/components/main/helpers.js | 63 | ||||
-rw-r--r-- | client/components/main/layouts.jade | 17 | ||||
-rw-r--r-- | client/components/main/popup.js | 16 | ||||
-rw-r--r-- | client/components/main/popup.styl | 585 | ||||
-rw-r--r-- | client/components/main/popup.tpl.jade | 13 | ||||
-rw-r--r-- | client/components/main/rendered.js | 40 | ||||
-rw-r--r-- | client/components/main/router.js | 5 | ||||
-rw-r--r-- | client/components/main/spinner.styl | 45 | ||||
-rw-r--r-- | client/components/main/spinner.tpl.jade | 6 | ||||
-rw-r--r-- | client/components/main/templates.html | 18 |
14 files changed, 1132 insertions, 0 deletions
diff --git a/client/components/main/events.js b/client/components/main/events.js new file mode 100644 index 00000000..beb90c5e --- /dev/null +++ b/client/components/main/events.js @@ -0,0 +1,8 @@ +Template.editor.events({ + // Pressing Ctrl+Enter should submit the form. + 'keydown textarea': function(event) { + if (event.keyCode === 13 && (event.metaKey || event.ctrlKey)) { + $(event.currentTarget).parents('form:first').submit(); + } + } +}); diff --git a/client/components/main/header.jade b/client/components/main/header.jade new file mode 100644 index 00000000..588c9b6e --- /dev/null +++ b/client/components/main/header.jade @@ -0,0 +1,40 @@ +template(name="header") + #header(class=currentBoard.colorClass) + //- + If the user is connected we display a small "quick-access" top bar that + list all starred boards with a link to go there. This is inspired by the + Reddit "subreddit" bar. + The first link goes to the boards page. + if currentUser + #header-quick-access + ul + li + +linkTo(route="Boards") + span.fa.fa-home + | All boards + each currentUser.starredBoards + li.separator - + li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}") + +linkTo(route="Board" data=this) + = title + else + li.current Star a board to add a shortcut in this bar. + + li + a.js-create-board + i.fa.fa-plus(title="Create a new board") + + +headerUserBar + + //- + The main bar is a colorful bar that provide all the meta-data for the + current page. This bar is contextual based. + If the user is not connected we display "sign in" and "log in" buttons. + #header-main-bar + if $.Session.get 'currentBoard' + +headerBoard + else + +headerTitle + +template(name="headerTitle") + h1 LibreBoard diff --git a/client/components/main/header.js b/client/components/main/header.js new file mode 100644 index 00000000..2a545309 --- /dev/null +++ b/client/components/main/header.js @@ -0,0 +1,10 @@ +Template.header.helpers({ + // Reactively set the color of the page from the color of the current board. + headerTemplate: function() { + return 'headerBoard'; + } +}); + +Template.header.events({ + 'click .js-create-board': Popup.open('createBoard') +}); diff --git a/client/components/main/header.styl b/client/components/main/header.styl new file mode 100644 index 00000000..1177d930 --- /dev/null +++ b/client/components/main/header.styl @@ -0,0 +1,266 @@ +@import 'nib' + +global-reset() + +#header + color: white + transition: background-color 0.4s + background: #27AE60 + + #header-quick-access + background-color: rgba(0, 0, 0, 0.2) + padding: 4px 10px + height: 16px + font-size: 12px + display: flex + + ul li, #header-user-bar + color: darken(white, 17%) + + a + color: inherit + text-decoration: none + + &:hover + color: white + + ul + flex: 1 + transition: opacity 0.2s + margin-left: 5px + + li + display: block + float: left + width: auto + color: darken(white, 15%) + padding: 0 4px 1px 4px + + &.separator + padding: 0 2px 1px 2px + + &.current + font-style: italic + + &:first-child .fa-home + margin-right: 5px + + #header-main-bar + height: 30px + padding: 8px + + h1 + font-size: 19px + line-height: 1.7em + margin: 0 20px 0 10px + float: left + + &.header-board-menu + cursor: pointer + + .fa-angle-down + font-size: 0.8em + // line-height: 1.1em + margin-left: 5px + + .board-header-starred .fa + color: yellow + + .board-header-members + float: right + + .member + display: block + width: 32px + height: @width + + .add-board-member + color: white + display: flex + align-items: center + justify-content: center + border: 1px solid white + height: 32px - 2px + width: @height + + i.fa-plus + margin-top: 2px + + .header-btn:last-child + margin-right: 0 + + + +// #header { +// background: #138871; +// height: 30px; +// overflow: hidden; +// padding: 5px; +// position: relative; +// z-index: 10; +// } + +// .header-logo { +// bottom: 0; +// display: block; +// height: 25px; +// left: 50%; +// position: absolute; +// top: 8px; +// width: 80px; +// margin-left: - @width/2; +// text-align: center; +// z-index: 2; +// opacity: .5; +// transition: opacity ease-in 85ms; +// color: white; +// font-size: 22px; +// text-decoration: none; +// background-image: url('/logos/white_logo.png'); + +// &:hover { +// opacity: .8; +// color: white; +// } +// } + +// .header-btn.header-btn-feedback { +// background: rgba(255, 255, 255, .1); +// background: linear-gradient(to bottom, rgba(255, 255, 255, .1) 0, rgba(255, 255, 255, .05) 100%); +// padding-left: 22px; +// margin-right: 16px; + +// .header-btn-icon { +// top: 1px; +// } +// } + +.header-btn { + border-radius: 3px; + user-select: none; + background: rgba(255, 255, 255, .3); + background: linear-gradient(to bottom, rgba(255, 255, 255, .3) 0, rgba(255, 255, 255, .2) 100%); + color: #f3f3f3; + display: block; + float: left; + font-weight: 700; + height: 30px; + line-height: 30px; + padding: 0 10px; + position: relative; + margin-right: 8px; + min-width: 30px; + text-decoration: none; + cursor: pointer; + + .header-btn-icon { + font-size: 16px; + line-height: 28px; + position: absolute; + top: 0; + left: 0; + } + + &.new-notifications { + background: #ba1212; + + &:hover { + background: #d11515; + } + } + + &.header-member .member { + margin: 0; + border-top-left-radius: 3px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; + + &:hover .member-avatar { + opacity: 1; + } + } + + &:hover { + background: rgba(255, 255, 255, .4); + background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%); + color: #fff; + + .header-btn-count { + background: #d11515; + } + } + + &:active { + background: rgba(255, 255, 255, .4); + background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%); + } + + &.upgrade { + margin-right: 16px; + + .icon-sm { + padding: 6px 2px 6px 4px; + } + } + + &.upgrade, + &.header-boards { + padding-left: 4px; + } + + &.header-boards { + padding-right: 4px; + } + + &.header-login, + &.header-signup { + padding: 0 12px; + } + + &.header-signup { + background: #48b512; + background: linear-gradient(to bottom, #48b512 0, #3d990f 100%); + + &:hover { + background: #3d990f; + background: linear-gradient(to bottom, #3d990f 0, #327d0c 100%); + } + + &:active { + background: #327d0c; + } + } + + &.header-go-to-boards { + padding: 0 8px 0 38px; + } + + &.header-go-to-boards .member { + border-top-left-radius: 3px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; + position: absolute; + left: 0; + } +} + +// .header-btn-text { +// padding: 0 8px; +// } + +// .header-notification-list ul { +// margin-top: 8px; +// } + +// .header-notification-list .action-comment { +// max-height: 250px; +// overflow-y: auto; +// } + +// .header-user { +// position: absolute; +// top: 5px; +// right: 0; +// } diff --git a/client/components/main/helpers.js b/client/components/main/helpers.js new file mode 100644 index 00000000..7ad602f1 --- /dev/null +++ b/client/components/main/helpers.js @@ -0,0 +1,63 @@ +var Helpers = { + error: function() { + return Session.get('error'); + }, + + toLowerCase: function(text) { + return text && text.toLowerCase(); + }, + + toUpperCase: function(text) { + return text && text.toUpperCase(); + }, + + firstChar: function(text) { + return text && text[0].toUpperCase(); + }, + + session: function(prop) { + return Session.get(prop); + }, + + getUser: function(userId) { + return Users.findOne(userId); + } +}; + +// Register all Helpers +_.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); }); + +// XXX I believe we should compute a HTML rendered field on the server that +// would handle markdown, emojies and user mentions. We can simply have two +// fields, one source, and one compiled version (in HTML) and send only the +// compiled version to most users -- who don't need to edit. +// In the meantime, all the transformation are done on the client using the +// Blaze API. +var at = HTML.CharRef({html: '@', str: '@'}); +Blaze.Template.registerHelper('mentions', new Template('mentions', function() { + var view = this; + var content = Blaze.toHTML(view.templateContentBlock); + var currentBoard = Session.get('currentBoard'); + var knowedUsers = _.map(currentBoard.members, function(member) { + member.username = Users.findOne(member.userId).username; + return member; + }); + + var mentionRegex = /\B@(\w*)/gi; + var currentMention, knowedUser, href, linkClass, linkValue, link; + while (currentMention = mentionRegex.exec(content)) { + + knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] }); + if (! knowedUser) + continue; + + linkValue = [' ', at, knowedUser.username]; + href = Router.url('Profile', { username: knowedUser.username }); + linkClass = 'atMention' + (knowedUser.userId === Meteor.userId() ? ' me' : ''); + link = HTML.A({ href: href, 'class': linkClass }, linkValue); + + content = content.replace(currentMention[0], Blaze.toHTML(link)); + } + + return HTML.Raw(content); +})); diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade new file mode 100644 index 00000000..18df4d9e --- /dev/null +++ b/client/components/main/layouts.jade @@ -0,0 +1,17 @@ +head + title LibreBoard + meta(name="viewport" + content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0") + link(rel="shortcut icon" href="/favicon.png") + +template(name="userFormsLayout") + h1.at-form-landing-logo + img(src="/logo.png" title="LibreBoard") + +yield + +template(name="defaultLayout") + #surface + +header + #content + +yield + diff --git a/client/components/main/popup.js b/client/components/main/popup.js new file mode 100644 index 00000000..53695d10 --- /dev/null +++ b/client/components/main/popup.js @@ -0,0 +1,16 @@ +Popup.template.events({ + click: function(evt) { + if (evt.originalEvent) { + evt.originalEvent.clickInPopup = true; + } + }, + 'click .js-back-view': function() { + Popup.back(); + }, + 'click .js-close-popover': function() { + Popup.close(); + }, + 'click .js-confirm': function() { + this.__afterConfirmAction.call(this); + } +}); diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl new file mode 100644 index 00000000..8c9993af --- /dev/null +++ b/client/components/main/popup.styl @@ -0,0 +1,585 @@ +@import 'nib' + +.pop-over + background: #fff + border-radius: 3px + border: 1px solid #dbdbdb + border-bottom-color: #c2c2c2 + box-shadow: 0 1px 6px rgba(0, 0, 0, .3) + display: none + overflow: hidden + position: absolute + width: 300px + z-index: 99999 + margin-top: 5px + + hr + margin: 4px -10px + width: 275px + 2*10px + + input[type="text"], + input[type="email"], + input[type="password"] + margin: 4px 0 12px + width: 100% + + input[type="file"] + width: 240px + + select + width: 100% + margin-bottom: 14px + + textarea + height: 72px + margin: 4px 0 12px + width: 100% + + .empty + margin: 0 + + img + max-width: 270px + + .custom-image img + height: 18px + left: 9px + top: 9px + width: 18px + + .title + line-height: 32px + + .header + height: 36px + position: relative + margin-bottom: 8px + background: #F7F7F7 + border-bottom: 1px solid #dcdcdc + color: darken(white, 60%) + + .header-title + display: block + line-height: 32px + padding-top: 4px + margin: 0 10px + font-weight: bold + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + + .back-btn, .close-btn + &:hover .icon-sm + color: darken(white, 80%) + + .back-btn + padding: 10px + float: left + + .close-btn + padding: 10px 10px 10px 4px + position: absolute + top: 0 + right: 0 + + .content + overflow-x: hidden + overflow-y: auto + padding: 0 10px 10px + max-height: 550px + + .quiet + padding: 6px 6px 4px + + &.search-over + background: #f0f0f0 + min-height: 114px + + .header + display: none + + .content + padding: 8px 4px 8px 10px + margin-right: 8px + + &::-webkit-scrollbar-button + display: block + height: 4px + width: 4px + +.select-members-list + margin-bottom: 8px + +.pop-over-list + + &.navigable li.not-selectable>a:hover, + li.not-selectable>a:hover + color: #8c8c8c + cursor: default + + .icon-sm + color: #a6a6a6 + + li > a + cursor: pointer + display: block + font-weight: 700 + padding: 6px 10px + position: relative + margin: 0 -10px + text-decoration: none + + .item-name + display: block + width: auto + padding-right: 22px + + &:hover + background-color: #005377 + color: #fff + + .sub-name, + .quiet + color: #eee + + .unread-indicator + background: #fff + + .icon-sm + color: #fff + + .sub-name + clear: both + color: #8c8c8c + display: block + font-size: 12px + font-weight: 400 + line-height: 15px + margin-top: 4px + + &.current + background-color: #e2e6e9 + + .unread-indicator + background: #2e85b8 + background: linear-gradient(to bottom, #2e85b8 0, #2b7cab 100%) + border-radius: 7px + display: block + height: 14px + opacity: 0 + position: absolute + right: 16px + top: 8px + width: 14px + + &.any + opacity: 1 + + &:active + background-color: #2e85b8 + + &.disabled + color: #8c8c8c + cursor: default + + .vis-icon + opacity: .35 + + .icon-sm + color: #a6a6a6 + + &:hover + background: none + + .sub-name, + .quiet + color: #8c8c8c + + .icon-sm + color: #a6a6a6 + + &:active + background: none + + &.inset li > a + border-radius: 3px + margin: 0 + + .pop-over-list.checkable + + .icon-check + display: none + position: absolute + top: 6px + right: 12px + + li.active a + padding-right: 28px + + .icon-check + display: block + + &.left-check + + .icon-check + right: auto + left: 10px + + li a + padding-right: 10px + padding-left: 30px + + li.active a + padding-right: 10px + + &.normal-weight li>a + font-weight: 400 + + &.navigable + + li > a:hover + background-color: transparent + color: #4d4d4d + + .sub-name, + .quiet + color: #8c8c8c + + .icon-sm + color: #a6a6a6 + + li.selected > a + background-color: #005377 + color: #fff + + .sub-name, + .quiet + color: #eee + + li.selected > a + + &.current + background-color: #005377 + + .unread-indicator + background: #fff + + .icon-sm + color: #fff + + &:active + background-color: #005377 + +.pop-over.miniprofile + + .header + border-bottom-color: transparent + height: 30px + position: absolute + right: 0 + top: 0 + width: 60px + z-index: 1 + + .header-title + display: none + + .pop-over-list + padding-top: 8px + +.mini-profile-info + margin-top: 8px + min-height: 56px + position: relative + + .member-large + position: absolute + top: 2px + left: 2px + + .info + margin: 0 0 0 64px + word-wrap: break-word + + h3 a + text-decoration: none + + &:hover + text-decoration: underline + +.pop-over.avdetail .header + border-bottom-color: transparent + height: 20px + position: absolute + top: 8px + left: 8px + right: 8px + z-index: 0 + +.pop-over.avdetail .header-title + display: none + +.pop-over.avdetail .content + text-align: center + +.pop-over.avdetail .mem-info + margin: 2px 24px 8px + position: relative + z-index: 1 + width: 222px + +.pop-over.avdetail .mem-info h3 a + text-decoration: none + +.pop-over.avdetail .mem-info h3 a:hover + text-decoration: underline + +.pop-over-label-list li, +.pop-over-member-list li + + &.disabled a + cursor:default + + &:not(.disabled):hover a + background-color: #005377 + color: #fff + + +.pop-over-label-list, +.pop-over-member-list, +.pop-over-emoji-list, +.pop-over-card-list + li + a + border-radius: 3px + display: block + height: 30px + line-height: 30px + overflow: hidden + position: relative + text-overflow: ellipsis + text-decoration: none + white-space: nowrap + padding: 4px + margin-bottom: 2px + + &.multi-line + line-height: 16px + + .member + margin-right: 8px + + .card-label + float: left + height: 30px + margin: 0 8px 0 0 + padding: 0 + width: 30px + + .option, + .icon-check + background-clip: content-box + background-origin: content-box + padding: 11px + position: absolute + top: 0 + right: 0 + + .sub-name + font-size: 12px + + + &:last-child a + margin-bottom: 0 + + &.disabled + opacity: .5 + + &.active a, + &.selected a + background: none + color: #4d4d4d + cursor: default + + .quiet + color: #8c8c8c + + &.email-invite + + .member + display: none + + a + padding: 0 10px + + &.selected a + background-color: #005377 + color: #fff + + .quiet + color: #eee + + .card-label + border-radius: 3px + + .icon-check + color: #fff + + &.active a .icon-check + display: block + + &.unconfirmed a.name + line-height: 16px + + &.options li + + &.selected a + padding-right: 28px + + .option + display: block + opacity: .5 + + &:hover + opacity: 1 + + &.disabled.selected a + padding-right: 0 + + .option + display: none + + + &.no-option.selected a + padding-right: 6px + + .option + display: none + + &.collapsed + + &.checkable li.active a + padding-right: 0 + + li + float: left + margin: 0 3px 3px 0 + + a + padding: 0 + margin: 0 + width: 30px + + .member + opacity: .8 + + .full-name + display: none + + &.selected a .member, + &.active.selected a .member + border-color: #005377 + opacity: .9 + + &.active a + + .member + border-color: #2e85b8 + opacity: 1 + + .icon-check + border-radius: 3px + background-color: #2e85b8 + bottom: 0 + color: #fff + display: block + padding: 0 + right: 0 + top: auto + + &.checkable li.active a + padding-right: 28px + + &.filtered li + display: none + + &.matches-filter + display: block + + &.limited li.exceeds-limit + display: none + +.pop-over-emoji-list li > a + padding: 2px 4px + + .emoji + margin: 0 6px + +.pop-over-card-list li > a + padding: 2px 4px + +.login-signup-popover + padding: 15px + + .form-tabs + display: none + + h1 + margin-bottom: 15px + + p + margin: 8px 0 + + .form-parts-container + position: relative + + .active-box + position: absolute + top: 0 + background: #e2e2e2 + border: 1px solid #c9c9c9 + border-radius: 3px + z-index: 1 + height: 100% + width: 49% + transition-property: all + transition-duration: .4s + opacity: 1 + + &.start + opacity: 0 + left: 25% + + .signup-form, + .login-form + position: relative + box-sizing: border-box + padding: 20px + width: 50% + z-index: 2 + opacity: .3 + transition-property: opacity + transition-duration: .2s + + .active + opacity: 1 + + + .js-signup-form-pos + left: 0 + + .login-form + position: absolute + top: 0 + + .login-form .icon-google + position: absolute + left: 5px + top: 3px + + .login-form .button.google + padding-left: 40px + margin: 0 0 15px 0 + + .js-login-form-pos + left: 50% diff --git a/client/components/main/popup.tpl.jade b/client/components/main/popup.tpl.jade new file mode 100644 index 00000000..ba24db0a --- /dev/null +++ b/client/components/main/popup.tpl.jade @@ -0,0 +1,13 @@ +.pop-over.clearfix( + class="{{#unless title}}miniprofile{{/unless}}" + class=currentBoard.colorClass + style="display:block; left:{{offset.left}}px; top:{{offset.top}}px;") + .header.clearfix + if hasPopupParent + a.back-btn.js-back-view + i.fa.fa-chevron-left + span.header-title= title + a.close-btn.js-close-popover + i.fa.fa-times + .content.clearfix + +Template.dynamic(template=popupName data=dataContext) diff --git a/client/components/main/rendered.js b/client/components/main/rendered.js new file mode 100644 index 00000000..787e8225 --- /dev/null +++ b/client/components/main/rendered.js @@ -0,0 +1,40 @@ +Template.editor.rendered = function() { + this.$('textarea').textcomplete([ + // Emojies + { + match: /\B:([\-+\w]*)$/, + search: function(term, callback) { + callback($.map(Emoji.values, function(emoji) { + return emoji.indexOf(term) === 0 ? emoji : null; + })); + }, + template: function(value) { + var image = '<img src="' + Emoji.baseImagePath + value + '.png"></img>'; + return image + value; + }, + replace: function(value) { + return ':' + value + ':'; + }, + index: 1 + }, + + // User mentions + { + match: /\B@(\w*)$/, + search: function(term, callback) { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + callback($.map(currentBoard.members, function(member) { + var username = Users.findOne(member.userId).username; + return username.indexOf(term) === 0 ? username : null; + })); + }, + template: function(value) { + return value; + }, + replace: function(username) { + return '@' + username + ' '; + }, + index: 1 + } + ]); +}; diff --git a/client/components/main/router.js b/client/components/main/router.js new file mode 100644 index 00000000..bae832e8 --- /dev/null +++ b/client/components/main/router.js @@ -0,0 +1,5 @@ +Router.route('/', { + name: 'Home', + redirectLoggedInUsers: true, + authenticated: true +}); diff --git a/client/components/main/spinner.styl b/client/components/main/spinner.styl new file mode 100644 index 00000000..f4b8cc86 --- /dev/null +++ b/client/components/main/spinner.styl @@ -0,0 +1,45 @@ +/* + * From https://github.com/tobiasahlin/SpinKit + * + * Usage: + * + * <div class="sk-spinner sk-spinner-wave"> + * <div class="sk-rect1"></div> + * <div class="sk-rect2"></div> + * <div class="sk-rect3"></div> + * <div class="sk-rect4"></div> + * <div class="sk-rect5"></div> + * </div> + * + */ + +.sk-spinner-wave { + + &.sk-spinner { + width: 50px; + height: 50px; + margin: auto; + margin-top: 30vh; + text-align: center; + font-size: 10px; + } + + div { + background-color: #333; + height: 100%; + width: 6px; + display: inline-block; + + animation: sk-waveStretchDelay 1.2s infinite ease-in-out; + } + + .sk-rect2 { animation-delay: -1.1s } + .sk-rect3 { animation-delay: -1.0s } + .sk-rect4 { animation-delay: -0.9s } + .sk-rect5 { animation-delay: -0.8s } +} + +@keyframes sk-waveStretchDelay { + 0%, 40%, 100% { transform: scaleY(0.4) } + 20% { transform: scaleY(1.0) } +} diff --git a/client/components/main/spinner.tpl.jade b/client/components/main/spinner.tpl.jade new file mode 100644 index 00000000..9310a6e5 --- /dev/null +++ b/client/components/main/spinner.tpl.jade @@ -0,0 +1,6 @@ +.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass) + .sk-rect1 + .sk-rect2 + .sk-rect3 + .sk-rect4 + .sk-rect5 diff --git a/client/components/main/templates.html b/client/components/main/templates.html new file mode 100644 index 00000000..e9be0f93 --- /dev/null +++ b/client/components/main/templates.html @@ -0,0 +1,18 @@ +<template name="notfound"> + {{ > message label='page-not-found'}} +</template> + +<template name='message'> + <div class="big-message quiet {{ color }}"> + <h1>{{_ label}}</h1> + {{#with pathFor route='Login'}} + <p>{{{_ 'page-maybe-private' this}}}</p> + {{/with}} + </div> +</template> + +<template name="editor"> + <textarea class="{{class}}" placeholder="{{_ 'comment-placeholder'}}" id="{{id}}" tabindex="1">{{> UI.contentBlock }}</textarea> +</template> + +<template name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template> |