FEATURE: group cards popup on mention clicks

This commit is contained in:
Jeff Wong 2018-04-13 17:43:18 -07:00
parent 3d7dbdedc0
commit 75e5f686fb
8 changed files with 490 additions and 261 deletions

View File

@ -1,115 +1,76 @@
import { wantsNewWindow } from 'discourse/lib/intercept-click'; import { wantsNewWindow } from 'discourse/lib/intercept-click';
import { propertyNotEqual, setting } from 'discourse/lib/computed';
import CleansUp from 'discourse/mixins/cleans-up'; import CleansUp from 'discourse/mixins/cleans-up';
import afterTransition from 'discourse/lib/after-transition'; import afterTransition from 'discourse/lib/after-transition';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { default as computed } from 'ember-addons/ember-computed-decorators';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
import User from 'discourse/models/user'; import User from 'discourse/models/user';
import Group from 'discourse/models/group';
import { userPath } from 'discourse/lib/url'; import { userPath } from 'discourse/lib/url';
import { durationTiny } from 'discourse/lib/formatter';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
const clickOutsideEventName = "mousedown.outside-user-card"; const clickOutsideEventName = "mousedown.outside-user-card";
const clickDataExpand = "click.discourse-user-card"; const clickDataExpand = "click.discourse-user-card";
const clickMention = "click.discourse-user-mention"; const clickMention = "click.discourse-user-mention";
const groupClickMention = "click.discourse-group-mention";
export default Ember.Component.extend(CleansUp, CanCheckEmails, { const maxMembersToDisplay = 10;
export default Ember.Component.extend(CleansUp, {
elementId: 'user-card', elementId: 'user-card',
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'], classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'],
allowBackgrounds: setting('allow_profile_backgrounds'),
postStream: Ember.computed.alias('topic.postStream'), postStream: Ember.computed.alias('topic.postStream'),
enoughPostsForFiltering: Ember.computed.gte('topicPostCount', 2),
viewingTopic: Ember.computed.match('currentPath', /^topic\./), 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'),
showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'),
visible: false, visible: false,
user: null, user: null,
group: null,
username: null, username: null,
avatar: null, avatar: null,
userLoading: null, userLoading: null,
cardTarget: null, cardTarget: null,
post: null, post: null,
cardType: null,
// If inside a topic // If inside a topic
topicPostCount: null, topicPostCount: null,
@computed('user.name') @computed('cardType')
nameFirst(name) { isUserShown(cardType) {
return !this.siteSettings.prioritize_username_in_ux && name && name.trim().length > 0; return cardType == 'user';
}, },
@computed('username', 'topicPostCount') @computed('cardType')
togglePostsLabel(username, count) { isGroupShown(cardType) {
return I18n.t("topic.filter_to", { username, count }); return cardType == 'group';
}, },
@computed('user.user_fields.@each.value') _showUser(username, $target) {
publicUserFields() { const args = { stats: false };
const siteUserFields = this.site.get('user_fields'); args.include_post_count_for = this.get('topic.id');
if (!Ember.isEmpty(siteUserFields)) {
const userFields = this.get('user.user_fields'); User.findByUsername(username, args).then(user => {
return siteUserFields.filterBy('show_on_user_card', true).sortBy('position').map(field => { if (user.topic_post_count) {
Ember.set(field, 'dasherized_name', field.get('name').dasherize()); this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
const value = userFields ? userFields[field.get('id')] : null; }
return Ember.isEmpty(value) ? null : Ember.Object.create({ value, field }); this.setProperties({ user, avatar: user, visible: true, cardType: 'user' });
}).compact();
} this._positionCard($target);
}).catch(() => this._close()).finally(() => this.set('userLoading', null));
}, },
@computed("user.trust_level") _showGroup(groupname, $target) {
removeNoFollow(trustLevel) { this.store.find("group", groupname).then(group => {
return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow; this.setProperties({ group, avatar: group, visible: true, cardType: 'group' });
this._positionCard($target);
if(!group.flair_url && !group.flair_bg_color) {
group.set('flair_url', 'fa-users');
}
group.set('limit', maxMembersToDisplay);
return group.findMembers();
}).catch(() => this._close()).finally(() => this.set('userLoading', null));
}, },
@computed('user.badge_count', 'user.featured_user_badges.length') _show(username, $target, userCardType) {
moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength,
@computed('user.card_badge.image')
hasCardBadgeImage: image => image && image.indexOf('fa-') !== 0,
@observes('user.card_background')
addBackground() {
if (!this.get('allowBackgrounds')) { return; }
const $this = this.$();
if (!$this) { return; }
const url = this.get('user.card_background');
const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`;
$this.css('background-image', bg);
},
@computed('user.time_read', 'user.recent_time_read')
showRecentTimeRead(timeRead, recentTimeRead) {
return timeRead !== recentTimeRead && recentTimeRead !== 0;
},
@computed('user.recent_time_read')
recentTimeRead(recentTimeReadSeconds) {
return durationTiny(recentTimeReadSeconds);
},
@computed('showRecentTimeRead', 'user.time_read', 'recentTimeRead')
timeReadTooltip(showRecent, timeRead, recentTimeRead) {
if (showRecent) {
return I18n.t('time_read_recently_tooltip', {time_read: durationTiny(timeRead), recent_time_read: recentTimeRead});
} else {
return I18n.t('time_read_tooltip', {time_read: durationTiny(timeRead)});
}
},
_show(username, $target) {
// No user card for anon // No user card for anon
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
return false; return false;
@ -141,17 +102,13 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
this.setProperties({ username, userLoading: username, cardTarget: target, post }); this.setProperties({ username, userLoading: username, cardTarget: target, post });
const args = { stats: false }; if(userCardType == 'group') {
args.include_post_count_for = this.get('topic.id'); this._showGroup(username, $target);
}
else if(userCardType == 'user') {
this._showUser(username, $target);
}
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; return false;
}, },
@ -179,14 +136,20 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
$('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => { $('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => {
if (wantsNewWindow(e)) { return; } if (wantsNewWindow(e)) { return; }
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
return this._show($target.data('user-card'), $target); return this._show($target.data('user-card'), $target, 'user');
}); });
$('#main-outlet').on(clickMention, 'a.mention', (e) => { $('#main-outlet').on(clickMention, 'a.mention', (e) => {
if (wantsNewWindow(e)) { return; } if (wantsNewWindow(e)) { return; }
const $target = $(e.target); const $target = $(e.target);
return this._show($target.text().replace(/^@/, ''), $target); return this._show($target.text().replace(/^@/, ''), $target, 'user');
}); });
$('#main-outlet').on(groupClickMention, 'a.mention-group', (e) => {
if (wantsNewWindow(e)) { return; }
const $target = $(e.target);
return this._show($target.text().replace(/^@/, ''), $target, 'group');
})
}, },
_positionCard(target) { _positionCard(target) {
@ -241,12 +204,14 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
this.setProperties({ this.setProperties({
visible: false, visible: false,
user: null, user: null,
group: null,
username: null, username: null,
avatar: null, avatar: null,
userLoading: null, userLoading: null,
cardTarget: null, cardTarget: null,
post: null, post: null,
topicPostCount: null topicPostCount: null,
cardType: null
}); });
}, },
@ -265,7 +230,7 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
willDestroyElement() { willDestroyElement() {
this._super(); this._super();
$('html').off(clickOutsideEventName); $('html').off(clickOutsideEventName);
$('#main').off(clickDataExpand).off(clickMention); $('#main').off(clickDataExpand).off(clickMention).off(groupClickMention);
}, },
actions: { actions: {
@ -284,6 +249,10 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
this.sendAction('composePrivateMessage', ...args); this.sendAction('composePrivateMessage', ...args);
}, },
messageGroup() {
this.sendAction('createNewMessageViaParams', this.get('group.name'));
},
togglePosts() { togglePosts() {
this.sendAction('togglePosts', this.get('user')); this.sendAction('togglePosts', this.get('user'));
this._close(); this._close();
@ -298,6 +267,11 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
this._close(); this._close();
}, },
showGroup() {
this.sendAction('showGroup', this.get('group'));
this._close();
},
checkEmail(user) { checkEmail(user) {
user.checkEmail(); user.checkEmail();
} }

View File

@ -0,0 +1,32 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
group: null,
showMoreMembers: Ember.computed.gt('moreMembersCount', 0),
@computed('group.user_count', 'group.members.length')
moreMembersCount: (memberCount, maxMemberDisplay) => memberCount - maxMemberDisplay,
@computed('group')
groupPath(group) {
return `${Discourse.BaseUri}/groups/${group.name}`;
},
actions: {
close() {
this.sendAction('close');
},
messageGroup() {
this.sendAction('messageGroup');
},
showGroup() {
this.sendAction('showGroup');
},
showUser(user) {
this.sendAction('showUser', user);
},
}
});

View File

@ -0,0 +1,116 @@
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import { propertyNotEqual, setting } from 'discourse/lib/computed';
import { durationTiny } from 'discourse/lib/formatter';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
export default Ember.Component.extend(CanCheckEmails, {
allowBackgrounds: setting('allow_profile_backgrounds'),
enoughPostsForFiltering: Ember.computed.gte('topicPostCount', 2),
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'),
showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'),
@computed('user.name')
nameFirst(name) {
return !this.siteSettings.prioritize_username_in_ux && name && name.trim().length > 0;
},
@computed('username', 'topicPostCount')
togglePostsLabel(username, count) {
return I18n.t("topic.filter_to", { username, count });
},
@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() {
if (!this.get('allowBackgrounds')) { return; }
const $this = this.$();
if (!$this) { return; }
const url = this.get('user.card_background');
const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`;
$this.css('background-image', bg);
},
@computed('user.time_read', 'user.recent_time_read')
showRecentTimeRead(timeRead, recentTimeRead) {
return timeRead !== recentTimeRead && recentTimeRead !== 0;
},
@computed('user.recent_time_read')
recentTimeRead(recentTimeReadSeconds) {
return durationTiny(recentTimeReadSeconds);
},
@computed('showRecentTimeRead', 'user.time_read', 'recentTimeRead')
timeReadTooltip(showRecent, timeRead, recentTimeRead) {
if (showRecent) {
return I18n.t('time_read_recently_tooltip', {time_read: durationTiny(timeRead), recent_time_read: recentTimeRead});
} else {
return I18n.t('time_read_tooltip', {time_read: durationTiny(timeRead)});
}
},
actions: {
close() {
this.sendAction('close');
},
cancelFilter() {
this.sendAction('cancelFilter');
},
composePrivateMessage(...args) {
this.sendAction('composePrivateMessage', ...args);
},
togglePosts() {
this.sendAction('togglePosts');
},
deleteUser() {
this.sendAction('deleteUser');
},
showUser() {
this.sendAction('showUser');
},
checkEmail(user) {
this.sendAction('showUser', user);
}
}
});

View File

@ -1,177 +1,33 @@
{{#if visible}} {{#if visible}}
<div class="card-content"> <div class="card-content">
{{#if isUserShown}}
<div class="user-card-avatar"> {{user-card-user-contents
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a> model=model
{{#if user.primary_group_name}} viewingTopic=viewingTopic
{{avatar-flair topicPostCount=topicPostCount
flairURL=user.primary_group_flair_url user=user
flairBgColor=user.primary_group_flair_bg_color username=username
flairColor=user.primary_group_flair_color userLoading=userLoading
groupName=user.primary_group_name}} cardTarget=cardTarget
{{/if}} avatar=avatar
{{plugin-outlet name="user-card-avatar-flair" args=(hash user=user) tagName='div'}} postStream=postStream
</div> close="close"
cancelFilter="cancelFilter"
<div class="names"> composePrivateMessage="composePrivateMessage"
<span> togglePosts="togglePosts"
<h1 class="{{staff}} {{new_user}} {{if nameFirst "full-name" "username"}}"> deleteUser="deleteUser"
<a href={{user.path}} {{action "showUser"}}>{{if nameFirst user.name (format-username username)}} {{user-status user currentUser=currentUser}}</a> showUser="showUser"
</h1> checkEmail="checkEmail"
{{plugin-outlet name="user-card-after-username" args=(hash user=user) tagName=''}} }}
{{#unless nameFirst}}
{{#if user.name}}
<h2 class='full-name'>{{user.name}}</h2>
{{/if}}
{{else}}
<h2 class='username'>{{username}}</h2>
{{/unless}}
{{#if user.title}}
<h2>{{user.title}}</h2>
{{/if}}
{{#if user.staged}}
<h2 class="staged">{{i18n 'user.staged'}}</h2>
{{/if}}
{{plugin-outlet name="user-card-post-names" args=(hash user=user) tagName='div'}}
</span>
</div>
<ul class="usercard-controls">
{{#if user.can_send_private_message_to_user}}
<li class='compose-pm'>
{{d-button
class="btn-primary"
action=(action "composePrivateMessage" user post)
icon="envelope"
label="user.private_message"}}
</li>
{{/if}}
{{#if showFilter}}
<li>
{{d-button
action=(action "togglePosts" user)
icon="filter"
translatedLabel=togglePostsLabel}}
</li>
{{/if}}
{{#if hasUserFilters}}
<li>
{{d-button
action="cancelFilter"
icon="times"
label="topic.filters.cancel"}}
</li>
{{/if}}
{{#if showDelete}}
<li>
{{d-button
class="btn-danger"
action=(action "deleteUser" user)
icon="exclamation-triangle"
label="admin.user.delete"}}
</li>
{{/if}}
</ul>
{{plugin-outlet
name="user-card-additional-controls"
args=(hash user=user close=(action "close"))
tagName=""}}
{{#if isSuspended}}
<div class='suspended'>
{{d-icon "ban"}}
<b>{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}</b><br>
<b>{{i18n 'user.suspended_reason'}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>{{/if}}
{{/if}} {{/if}}
{{#if isGroupShown}}
{{#if user.card_badge}} {{user-card-group-contents
{{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}} group=group
{{icon-or-image user.card_badge.image title=user.card_badge.name}} close="close"
{{/link-to}} messageGroup="messageGroup"
{{/if}} showGroup="showGroup"
showUser="showUser"
{{#if hasLocationOrWebsite}} }}
<div class="location-and-website">
{{#if user.location}}
<span class='location'>{{d-icon "map-marker"}} <span>{{user.location}}</span></span>
{{/if}}
{{#if user.website_name}}
<span class='website-name'>
{{d-icon "globe"}}
{{#if linkWebsite}}
<a href={{user.website}} rel={{unless removeNoFollow 'nofollow noopener'}} target="_blank">{{user.website_name}}</a>
{{else}}
<span title={{user.website}}>{{user.website_name}}</span>
{{/if}}
</span>
{{/if}}
{{plugin-outlet name="user-card-location-and-website" args=(hash user=user)}}
</div>
{{/if}}
{{#if user}}
<div class="metadata">
{{#if user.last_posted_at}}
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
{{/if}}
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
<h3 title="{{timeReadTooltip}}">
<span class='desc'>{{i18n 'time_read'}}</span>
{{format-duration user.time_read}}
{{#if showRecentTimeRead}}
<span>({{i18n 'time_read_recently' time_read=recentTimeRead}})</span>
{{/if}}
</h3>
{{#if showCheckEmail}}
<h3 class="email">
{{d-icon "envelope-o" title="user.email.title"}}
{{#if user.email}}
{{user.email}}
{{else}}
{{d-button action="checkEmail" actionParam=user icon="envelope-o" label="admin.users.check_email.text" class="btn-primary"}}
{{/if}}
</h3>
{{/if}}
{{plugin-outlet name="user-card-metadata" args=(hash user=user)}}
</div>
{{/if}}
{{#if publicUserFields}}
<div class="public-user-fields">
{{#each publicUserFields as |uf|}}
{{#if uf.value}}
<div class="public-user-field {{uf.field.dasherized_name}}">
<span class="user-field-name">{{uf.field.name}}:</span>
<span class="user-field-value">{{uf.value}}</span>
</div>
{{/if}}
{{/each}}
</div>
{{/if}}
{{#if showBadges}}
<div class="badge-section">
{{#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}}
</div>
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}

View File

@ -0,0 +1,50 @@
<div class="group-card-avatar">
<a href={{groupPath}} {{action "showGroup"}} class="card-huge-avatar">
{{avatar-flair
flairURL=group.flair_url
flairBgColor=group.flair_bg_color
flairColor=group.flair_color
groupName=group.name}}
</a>
</div>
<div class="names">
<span>
<h1 class="{{ group.name }}">
<a href={{groupPath}} {{action "showGroup"}}>{{ group.name }}</a>
</h1>
{{#if group.full_name}}
<h2 class='full-name'>{{group.full_name}}</h2>
{{else}}
<h2 class='username'>{{group.name}}</h2>
{{/if}}
</span>
</div>
<div class="usercard-controls group-details-button">
{{group-membership-button
model=group
showLogin='showLogin'}}
{{#if group.messageable}}
{{d-button
action="messageGroup"
class="btn-primary group-message-button inline"
icon="envelope"
label="groups.message"}}
{{/if}}
</div>
<div class="metadata">
<h3><a href={{groupPath}} {{action "showGroup"}}>{{ group.user_count }} {{i18n 'groups.user_count'}}</a></h3>
</div>
<div class="members metadata">
<span>
{{#each group.members as |user|}}
<a href={{user.path}} {{action "showUser" user}} class="card-tiny-avatar">{{bound-avatar user "tiny"}}</a>
{{/each}}
{{#if showMoreMembers}}
<a href={{groupPath}} {{action "showGroup"}}>+{{ moreMembersCount }} {{i18n "more"}}</a>
{{/if}}
</span>
</div>

View File

@ -0,0 +1,172 @@
<div class="user-card-avatar">
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a>
{{#if user.primary_group_name}}
{{avatar-flair
flairURL=user.primary_group_flair_url
flairBgColor=user.primary_group_flair_bg_color
flairColor=user.primary_group_flair_color
groupName=user.primary_group_name}}
{{/if}}
{{plugin-outlet name="user-card-avatar-flair" args=(hash user=user) tagName='div'}}
</div>
<div class="names">
<span>
<h1 class="{{staff}} {{new_user}} {{if nameFirst "full-name" "username"}}">
<a href={{user.path}} {{action "showUser"}}>{{if nameFirst user.name (format-username username)}} {{user-status user currentUser=currentUser}}</a>
</h1>
{{plugin-outlet name="user-card-after-username" args=(hash user=user) tagName=''}}
{{#unless nameFirst}}
{{#if user.name}}
<h2 class='full-name'>{{user.name}}</h2>
{{/if}}
{{else}}
<h2 class='username'>{{username}}</h2>
{{/unless}}
{{#if user.title}}
<h2>{{user.title}}</h2>
{{/if}}
{{#if user.staged}}
<h2 class="staged">{{i18n 'user.staged'}}</h2>
{{/if}}
{{plugin-outlet name="user-card-post-names" args=(hash user=user) tagName='div'}}
</span>
</div>
<ul class="usercard-controls">
{{#if user.can_send_private_message_to_user}}
<li class='compose-pm'>
{{d-button
class="btn-primary"
action=(action "composePrivateMessage" user post)
icon="envelope"
label="user.private_message"}}
</li>
{{/if}}
{{#if showFilter}}
<li>
{{d-button
action=(action "togglePosts" user)
icon="filter"
translatedLabel=togglePostsLabel}}
</li>
{{/if}}
{{#if hasUserFilters}}
<li>
{{d-button
action="cancelFilter"
icon="times"
label="topic.filters.cancel"}}
</li>
{{/if}}
{{#if showDelete}}
<li>
{{d-button
class="btn-danger"
action=(action "deleteUser" user)
icon="exclamation-triangle"
label="admin.user.delete"}}
</li>
{{/if}}
</ul>
{{plugin-outlet
name="user-card-additional-controls"
args=(hash user=user close=(action "close"))
tagName=""}}
{{#if isSuspended}}
<div class='suspended'>
{{d-icon "ban"}}
<b>{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}</b><br>
<b>{{i18n 'user.suspended_reason'}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>{{/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}}
<div class="location-and-website">
{{#if user.location}}
<span class='location'>{{d-icon "map-marker"}} <span>{{user.location}}</span></span>
{{/if}}
{{#if user.website_name}}
<span class='website-name'>
{{d-icon "globe"}}
{{#if linkWebsite}}
<a href={{user.website}} rel={{unless removeNoFollow 'nofollow noopener'}} target="_blank">{{user.website_name}}</a>
{{else}}
<span title={{user.website}}>{{user.website_name}}</span>
{{/if}}
</span>
{{/if}}
{{plugin-outlet name="user-card-location-and-website" args=(hash user=user)}}
</div>
{{/if}}
{{#if user}}
<div class="metadata">
{{#if user.last_posted_at}}
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
{{/if}}
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
<h3 title="{{timeReadTooltip}}">
<span class='desc'>{{i18n 'time_read'}}</span>
{{format-duration user.time_read}}
{{#if showRecentTimeRead}}
<span>({{i18n 'time_read_recently' time_read=recentTimeRead}})</span>
{{/if}}
</h3>
{{#if showCheckEmail}}
<h3 class="email">
{{d-icon "envelope-o" title="user.email.title"}}
{{#if user.email}}
{{user.email}}
{{else}}
{{d-button action="checkEmail" actionParam=user icon="envelope-o" label="admin.users.check_email.text" class="btn-primary"}}
{{/if}}
</h3>
{{/if}}
{{plugin-outlet name="user-card-metadata" args=(hash user=user)}}
</div>
{{/if}}
{{#if publicUserFields}}
<div class="public-user-fields">
{{#each publicUserFields as |uf|}}
{{#if uf.value}}
<div class="public-user-field {{uf.field.dasherized_name}}">
<span class="user-field-name">{{uf.field.name}}:</span>
<span class="user-field-value">{{uf.value}}</span>
</div>
{{/if}}
{{/each}}
</div>
{{/if}}
{{#if showBadges}}
<div class="badge-section">
{{#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}}
</div>
{{/if}}

View File

@ -4,4 +4,5 @@
showUser="showUser" showUser="showUser"
togglePosts="togglePosts" togglePosts="togglePosts"
composePrivateMessage="composePrivateMessage" composePrivateMessage="composePrivateMessage"
createNewMessageViaParams="createNewMessageViaParams"
deleteUser="deleteUser"}} deleteUser="deleteUser"}}

View File

@ -198,6 +198,34 @@ $user_card_background: $secondary;
margin-top: -53px; margin-top: -53px;
} }
.group-card-avatar {
float: left;
margin-right: 10px;
margin-top: 0px;
.avatar-flair {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
background-repeat: no-repeat;
background-position: center;
color: $primary;
i {
margin: auto;
font-size: $font-up-4;
}
}
}
.members {
a {
color: lighten($primary, 40%);
&:hover {
color: $primary;
}
}
}
p { p {
margin: 0 0 5px 0; margin: 0 0 5px 0;
} }