diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 new file mode 100644 index 00000000000..ccac108e9f0 --- /dev/null +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -0,0 +1,275 @@ +import { wantsNewWindow } from 'discourse/lib/intercept-click'; +import { propertyNotEqual, setting } from 'discourse/lib/computed'; +import CleansUp from 'discourse/mixins/cleans-up'; +import afterTransition from 'discourse/lib/after-transition'; +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import DiscourseURL from 'discourse/lib/url'; +import User from 'discourse/models/user'; + +const clickOutsideEventName = "mousedown.outside-user-card"; +const clickDataExpand = "click.discourse-user-card"; +const clickMention = "click.discourse-user-mention"; + +export default Ember.Component.extend(CleansUp, { + elementId: 'user-card', + classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage'], + allowBackgrounds: setting('allow_profile_backgrounds'), + + postStream: Ember.computed.alias('topic.postStream'), + enoughPostsForFiltering: Ember.computed.gte('topicPostCount', 2), + viewingTopic: Ember.computed.match('currentPath', /^topic\./), + viewingAdmin: Ember.computed.match('currentPath', /^admin\./), + showFilter: Ember.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'), + showName: propertyNotEqual('user.name', 'user.username'), + hasUserFilters: Ember.computed.gt('postStream.userFilters.length', 0), + isSuspended: Ember.computed.notEmpty('user.suspend_reason'), + showBadges: setting('enable_badges'), + showMoreBadges: Ember.computed.gt('moreBadgesCount', 0), + showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"), + linkWebsite: Ember.computed.not('user.isBasic'), + hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'), + + visible: false, + user: null, + username: null, + avatar: null, + userLoading: null, + cardTarget: null, + post: null, + + // If inside a topic + topicPostCount: null, + + @computed('user.name') + nameFirst(name) { + return !this.siteSettings.prioritize_username_in_ux && name && name.trim().length > 0; + }, + + @computed('user.user_fields.@each.value') + publicUserFields() { + const siteUserFields = this.site.get('user_fields'); + if (!Ember.isEmpty(siteUserFields)) { + const userFields = this.get('user.user_fields'); + return siteUserFields.filterBy('show_on_user_card', true).sortBy('position').map(field => { + Ember.set(field, 'dasherized_name', field.get('name').dasherize()); + const value = userFields ? userFields[field.get('id')] : null; + return Ember.isEmpty(value) ? null : Ember.Object.create({ value, field }); + }).compact(); + } + }, + + @computed("user.trust_level") + removeNoFollow(trustLevel) { + return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow; + }, + + @computed('user.badge_count', 'user.featured_user_badges.length') + moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength, + + @computed('user.card_badge.image') + hasCardBadgeImage: image => image && image.indexOf('fa-') !== 0, + + @observes('user.card_background') + addBackground() { + const url = this.get('user.card_background'); + + if (!this.get('allowBackgrounds')) { return; } + + const $this = this.$(); + if (!$this) { return; } + + if (Ember.isEmpty(url)) { + $this.css('background-image', '').addClass('no-bg'); + } else { + $this.css('background-image', `url(${Discourse.getURLWithCDN(url)})`).removeClass('no-bg'); + } + }, + + _show(username, $target) { + // No user card for anon + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { + return true; + } + + // XSS protection (should be encapsulated) + username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, ""); + + // Don't show on mobile + if (this.site.mobileView) { + DiscourseURL.routeTo(`/users/${username}`); + return false; + } + + const currentUsername = this.get('username'); + if (username === currentUsername && this.get('userLoading') === username) { + return; + } + + const postId = $target.parents('article').data('post-id'); + + const wasVisible = this.get('visible'); + const previousTarget = this.get('cardTarget'); + const target = $target[0]; + if (wasVisible) { + this._close(); + if (target === previousTarget) { return; } + } + + const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; + this.setProperties({ username, userLoading: username, cardTarget: target, post }); + + const args = { stats: false }; + args.include_post_count_for = this.get('topic.id'); + args.skip_track_visit = true; + + User.findByUsername(username, args).then(user => { + if (user.topic_post_count) { + this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]); + } + this.setProperties({ user, avatar: user, visible: true }); + + this._positionCard($target); + }).catch(() => this._close()).finally(() => this.set('userLoading', null)); + + return false; + }, + + didInsertElement() { + this._super(); + afterTransition(this.$(), this._hide.bind(this)); + + $('html').off(clickOutsideEventName) + .on(clickOutsideEventName, (e) => { + if (this.get('visible')) { + const $target = $(e.target); + if ($target.closest('[data-user-card]').data('userCard') || + $target.closest('a.mention').length > 0 || + $target.closest('#user-card').length > 0) { + return; + } + + this._close(); + } + + return true; + }); + + $('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => { + if (wantsNewWindow(e)) { return; } + const $target = $(e.currentTarget); + return this._show($target.data('user-card'), $target); + }); + + $('#main-outlet').on(clickMention, 'a.mention', (e) => { + if (wantsNewWindow(e)) { return; } + const $target = $(e.target); + return this._show($target.text().replace(/^@/, ''), $target); + }); + }, + + _positionCard(target) { + const rtl = ($('html').css('direction')) === 'rtl'; + if (!target) { return; } + const width = this.$().width(); + + Ember.run.schedule('afterRender', () => { + if (target) { + let position = target.offset(); + if (position) { + + if (rtl) { // The site direction is rtl + position.right = $(window).width() - position.left + 10; + position.left = 'auto'; + let overage = ($(window).width() - 50) - (position.right + width); + if (overage < 0) { + position.right += overage; + position.top += target.height() + 48; + } + } else { // The site direction is ltr + position.left += target.width() + 10; + + let overage = ($(window).width() - 50) - (position.left + width); + if (overage < 0) { + position.left += overage; + position.top += target.height() + 48; + } + } + + position.top -= $('#main-outlet').offset().top; + this.$().css(position); + } + + // After the card is shown, focus on the first link + // + // note: we DO NOT use afterRender here cause _positionCard may + // run afterwards, if we allowed this to happen the usercard + // may be offscreen and we may scroll all the way to it on focus + Ember.run.next(null, () => this.$('a:first').focus() ); + } + }); + }, + + _hide() { + if (!this.get('visible')) { + this.$().css({left: -9999, top: -9999}); + } + }, + + _close() { + this.setProperties({ + visible: false, + user: null, + username: null, + avatar: null, + userLoading: null, + cardTarget: null, + post: null, + topicPostCount: null + }); + }, + + cleanUp() { + this._close(); + }, + + keyUp(e) { + if (e.keyCode === 27) { // ESC + const target = this.get('cardTarget'); + this._close(); + target.focus(); + } + }, + + willDestroyElement() { + this._super(); + $('html').off(clickOutsideEventName); + $('#main').off(clickDataExpand).off(clickMention); + }, + + actions: { + cancelFilter() { + const postStream = this.get('postStream'); + postStream.cancelFilter(); + postStream.refresh(); + this._close(); + }, + + composePrivateMessage(...args) { + this.sendAction('composePrivateMessage', ...args); + }, + + togglePosts() { + this.sendAction('togglePosts', this.get('user')); + this._close(); + }, + + deleteUser() { + this.sendAction('deleteUser', this.get('user')); + }, + + showUser() { + this.sendAction('showUser', this.get('user')); + this._close(); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 985d977216c..bef0267803d 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -1,151 +1,15 @@ -import DiscourseURL from 'discourse/lib/url'; -import { propertyNotEqual, setting } from 'discourse/lib/computed'; -import computed from 'ember-addons/ember-computed-decorators'; - export default Ember.Controller.extend({ topic: Ember.inject.controller(), application: Ember.inject.controller(), - visible: false, - user: null, - username: null, - avatar: null, - userLoading: null, - cardTarget: null, - post: null, - - // If inside a topic - topicPostCount: null, - - postStream: Em.computed.alias('topic.model.postStream'), - enoughPostsForFiltering: Em.computed.gte('topicPostCount', 2), - viewingTopic: Em.computed.match('application.currentPath', /^topic\./), - viewingAdmin: Em.computed.match('application.currentPath', /^admin\./), - showFilter: Em.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'), - showName: propertyNotEqual('user.name', 'user.username'), - hasUserFilters: Em.computed.gt('postStream.userFilters.length', 0), - isSuspended: Em.computed.notEmpty('user.suspend_reason'), - showBadges: setting('enable_badges'), - showMoreBadges: Em.computed.gt('moreBadgesCount', 0), - showDelete: Em.computed.and("viewingAdmin", "showName", "user.canBeDeleted"), - linkWebsite: Em.computed.not('user.isBasic'), - hasLocationOrWebsite: Em.computed.or('user.location', 'user.website_name'), - - @computed('user.name') - nameFirst(name) { - return !this.get('siteSettings.prioritize_username_in_ux') && name && name.trim().length > 0; - }, - - @computed('user.user_fields.@each.value') - publicUserFields() { - const siteUserFields = this.site.get('user_fields'); - if (!Ember.isEmpty(siteUserFields)) { - const userFields = this.get('user.user_fields'); - return siteUserFields.filterBy('show_on_user_card', true).sortBy('position').map(field => { - Ember.set(field, 'dasherized_name', field.get('name').dasherize()); - const value = userFields ? userFields[field.get('id')] : null; - return Ember.isEmpty(value) ? null : Ember.Object.create({ value, field }); - }).compact(); - } - }, - - @computed("user.trust_level") - removeNoFollow(trustLevel) { - return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow; - }, - - moreBadgesCount: function() { - return this.get('user.badge_count') - this.get('user.featured_user_badges.length'); - }.property('user.badge_count', 'user.featured_user_badges.[]'), - - hasCardBadgeImage: function() { - const img = this.get('user.card_badge.image'); - return img && img.indexOf('fa-') !== 0; - }.property('user.card_badge.image'), - - show(username, postId, target) { - // XSS protection (should be encapsulated) - username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, ""); - - // No user card for anon - if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { - return; - } - - // Don't show on mobile - if (this.site.mobileView) { - const url = "/users/" + username; - DiscourseURL.routeTo(url); - return; - } - - const currentUsername = this.get('username'), - wasVisible = this.get('visible'), - previousTarget = this.get('cardTarget'), - post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; - - if (username === currentUsername && this.get('userLoading') === username) { - // debounce - return; - } - - if (wasVisible) { - this.close(); - if (target === previousTarget) { - return; // Same target, close it without loading the new user card - } - } - - this.setProperties({ username, userLoading: username, cardTarget: target, post }); - - const args = { stats: false }; - args.include_post_count_for = this.get('topic.model.id'); - args.skip_track_visit = true; - - return Discourse.User.findByUsername(username, args).then((user) => { - if (user.topic_post_count) { - this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]); - } - this.setProperties({ user, avatar: user, visible: true }); - }).catch((error) => { - this.close(); - throw error; - }).finally(() => { - this.set('userLoading', null); - }); - }, - - close() { - this.setProperties({ - visible: false, - user: null, - username: null, - avatar: null, - userLoading: null, - cardTarget: null, - post: null, - topicPostCount: null - }); - }, - actions: { togglePosts(user) { const topicController = this.get('topic'); topicController.send('toggleParticipant', user); - this.close(); }, - cancelFilter() { - const postStream = this.get('postStream'); - postStream.cancelFilter(); - postStream.refresh(); - this.close(); - }, - - showUser() { - this.transitionToRoute('user', this.get('user')); - this.close(); + showUser(user) { + this.transitionToRoute('user', user); } } - }); diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs new file mode 100644 index 00000000000..641351cc00d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -0,0 +1,124 @@ +{{#if visible}} +
+ +
+ {{bound-avatar avatar "huge"}} + {{#if user.primary_group_name}} + {{mount-widget widget="avatar-flair" args=user}} + {{/if}} +
+ +
+ +

+ {{if nameFirst user.name username}} {{user-status user currentUser=currentUser}} +

+ + {{#unless nameFirst}} + {{#if user.name}} +

{{user.name}}

+ {{/if}} + {{else}} +

{{username}}

+ {{/unless}} + + {{#if user.title}} +

{{user.title}}

+ {{/if}} + + {{plugin-outlet "user-card-post-names"}} +
+
+ + + + {{#if isSuspended}} +
+ {{fa-icon "ban"}} + {{i18n 'user.suspended_notice' date=user.suspendedTillDate}}
+ {{i18n 'user.suspended_reason'}} {{user.suspend_reason}} +
+ {{else}} + {{#if user.bio_cooked}}
{{text-overflow class="overflow" text=user.bio_excerpt}}
{{/if}} + {{/if}} + + {{#if user.card_badge}} + {{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}} + {{icon-or-image user.card_badge.image title=user.card_badge.name}} + {{/link-to}} + {{/if}} + + {{#if hasLocationOrWebsite}} +
+ {{#if user.location}} + {{fa-icon "map-marker"}} {{user.location}} + {{/if}} + + {{#if user.website_name}} + + {{fa-icon "globe"}} + {{#if linkWebsite}} + {{user.website_name}} + {{else}} + {{user.website_name}} + {{/if}} + + {{/if}} + + {{plugin-outlet "user-card-location-and-website"}} +
+ {{/if}} + + {{#if user}} +
+ {{#if user.last_posted_at}} +

{{i18n 'last_post'}} {{format-date user.last_posted_at leaveAgo="true"}}

+ {{/if}} +

{{i18n 'joined'}} {{format-date user.created_at leaveAgo="true"}}

+ {{plugin-outlet "user-card-metadata"}} +
+ {{/if}} + + {{#if publicUserFields}} +
+ {{#each publicUserFields as |uf|}} + {{#if uf.value}} +
+ {{uf.field.name}}: + {{uf.value}} +
+ {{/if}} + {{/each}} +
+ {{/if}} + + {{#if showBadges}} +
+ {{#each user.featured_user_badges as |ub|}} + {{user-badge badge=ub.badge user=user}} + {{/each}} + {{#if showMoreBadges}} + {{#link-to 'user.badges' user class="btn more-user-badges"}} + {{i18n 'badges.more_badges' count=moreBadgesCount}} + {{/link-to}} + {{/if}} +
+ {{/if}} +
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/user-card.hbs b/app/assets/javascripts/discourse/templates/user-card.hbs index 48f3c5faa5d..6a0b2ade382 100644 --- a/app/assets/javascripts/discourse/templates/user-card.hbs +++ b/app/assets/javascripts/discourse/templates/user-card.hbs @@ -1,124 +1,7 @@ -{{#if visible}} -
- -
- {{bound-avatar avatar "huge"}} - {{#if user.primary_group_name}} - {{mount-widget widget="avatar-flair" args=user}} - {{/if}} -
- -
- -

- {{if nameFirst user.name username}} {{user-status user currentUser=currentUser}} -

- - {{#unless nameFirst}} - {{#if user.name}} -

{{user.name}}

- {{/if}} - {{else}} -

{{username}}

- {{/unless}} - - {{#if user.title}} -

{{user.title}}

- {{/if}} - - {{plugin-outlet "user-card-post-names"}} -
-
- - - - {{#if isSuspended}} -
- {{fa-icon "ban"}} - {{i18n 'user.suspended_notice' date=user.suspendedTillDate}}
- {{i18n 'user.suspended_reason'}} {{user.suspend_reason}} -
- {{else}} - {{#if user.bio_cooked}}
{{text-overflow class="overflow" text=user.bio_excerpt}}
{{/if}} - {{/if}} - - {{#if user.card_badge}} - {{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}} - {{icon-or-image user.card_badge.image title=user.card_badge.name}} - {{/link-to}} - {{/if}} - - {{#if hasLocationOrWebsite}} -
- {{#if user.location}} - {{fa-icon "map-marker"}} {{user.location}} - {{/if}} - - {{#if user.website_name}} - - {{fa-icon "globe"}} - {{#if linkWebsite}} - {{user.website_name}} - {{else}} - {{user.website_name}} - {{/if}} - - {{/if}} - - {{plugin-outlet "user-card-location-and-website"}} -
- {{/if}} - - {{#if user}} -
- {{#if user.last_posted_at}} -

{{i18n 'last_post'}} {{format-date user.last_posted_at leaveAgo="true"}}

- {{/if}} -

{{i18n 'joined'}} {{format-date user.created_at leaveAgo="true"}}

- {{plugin-outlet "user-card-metadata"}} -
- {{/if}} - - {{#if publicUserFields}} -
- {{#each publicUserFields as |uf|}} - {{#if uf.value}} -
- {{uf.field.name}}: - {{uf.value}} -
- {{/if}} - {{/each}} -
- {{/if}} - - {{#if showBadges}} -
- {{#each user.featured_user_badges as |ub|}} - {{user-badge badge=ub.badge user=user}} - {{/each}} - {{#if showMoreBadges}} - {{#link-to 'user.badges' user class="btn more-user-badges"}} - {{i18n 'badges.more_badges' count=moreBadgesCount}} - {{/link-to}} - {{/if}} -
- {{/if}} -
-{{/if}} +{{user-card-contents + currentPath=application.currentPath + topic=topic.model + showUser="showUser" + togglePosts="togglePosts" + composePrivateMessage="composePrivateMessage" + deleteUser="deleteUser"}} diff --git a/app/assets/javascripts/discourse/views/user-card.js.es6 b/app/assets/javascripts/discourse/views/user-card.js.es6 deleted file mode 100644 index f5e58c630eb..00000000000 --- a/app/assets/javascripts/discourse/views/user-card.js.es6 +++ /dev/null @@ -1,149 +0,0 @@ -import { wantsNewWindow } from 'discourse/lib/intercept-click'; -import { setting } from 'discourse/lib/computed'; -import CleansUp from 'discourse/mixins/cleans-up'; -import afterTransition from 'discourse/lib/after-transition'; - -const clickOutsideEventName = "mousedown.outside-user-card", - clickDataExpand = "click.discourse-user-card", - clickMention = "click.discourse-user-mention"; - -export default Ember.View.extend(CleansUp, { - elementId: 'user-card', - classNameBindings: ['controller.visible:show', 'controller.showBadges', 'controller.hasCardBadgeImage'], - allowBackgrounds: setting('allow_profile_backgrounds'), - - addBackground: function() { - const url = this.get('controller.user.card_background'); - - if (!this.get('allowBackgrounds')) { return; } - - const $this = this.$(); - if (!$this) { return; } - - if (Ember.isEmpty(url)) { - $this.css('background-image', '').addClass('no-bg'); - } else { - $this.css('background-image', "url(" + Discourse.getURLWithCDN(url) + ")").removeClass('no-bg'); - } - }.observes('controller.user.card_background'), - - _setup: function() { - afterTransition(this.$(), this._hide.bind(this)); - - $('html').off(clickOutsideEventName) - .on(clickOutsideEventName, (e) => { - if (this.get('controller.visible')) { - const $target = $(e.target); - if ($target.closest('[data-user-card]').data('userCard') || - $target.closest('a.mention').length > 0 || - $target.closest('#user-card').length > 0) { - return; - } - - this.get('controller').close(); - } - - return true; - }); - - const expand = (username, $target) => { - const postId = $target.parents('article').data('post-id'), - user = this.get('controller').show(username, postId, $target[0]); - if (user !== undefined) { - user.then( () => this._willShow($target) ).catch( () => this._hide() ); - } else { - this._hide(); - } - return false; - }; - - $('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => { - if (wantsNewWindow(e)) { return; } - - const $target = $(e.currentTarget), - username = $target.data('user-card'); - return expand(username, $target); - }); - - $('#main-outlet').on(clickMention, 'a.mention', (e) => { - if (wantsNewWindow(e)) { return; } - - const $target = $(e.target), - username = $target.text().replace(/^@/, ''); - return expand(username, $target); - }); - this.appEvents.on('usercard:shown', this, '_shown'); - }.on('didInsertElement'), - - _shown() { - // After the card is shown, focus on the first link - // - // note: we DO NOT use afterRender here cause _willShow may - // run after _shown, if we allowed this to happen the usercard - // may be offscreen and we may scroll all the way to it on focus - Ember.run.next(null, () => this.$('a:first').focus() ); - }, - - _willShow(target) { - const rtl = ($('html').css('direction')) === 'rtl'; - if (!target) { return; } - const width = this.$().width(); - - Ember.run.schedule('afterRender', () => { - if (target) { - let position = target.offset(); - if (position) { - - if (rtl) { // The site direction is rtl - position.right = $(window).width() - position.left + 10; - position.left = 'auto'; - let overage = ($(window).width() - 50) - (position.right + width); - if (overage < 0) { - position.right += overage; - position.top += target.height() + 48; - } - } else { // The site direction is ltr - position.left += target.width() + 10; - - let overage = ($(window).width() - 50) - (position.left + width); - if (overage < 0) { - position.left += overage; - position.top += target.height() + 48; - } - } - - position.top -= $('#main-outlet').offset().top; - this.$().css(position); - } - this.appEvents.trigger('usercard:shown'); - } - }); - }, - - _hide() { - if (!this.get('controller.visible')) { - this.$().css({left: -9999, top: -9999}); - } - }, - - cleanUp() { - this.get('controller').close(); - }, - - keyUp(e) { - if (e.keyCode === 27) { // ESC - const target = this.get('controller.cardTarget'); - this.cleanUp(); - target.focus(); - } - }, - - _removeEvents: function() { - $('html').off(clickOutsideEventName); - - $('#main').off(clickDataExpand).off(clickMention); - - this.appEvents.off('usercard:shown', this, '_shown'); - }.on('willDestroyElement') - -});