diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index cac2ea1f0..02fa08fa8 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -9,12 +9,14 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { return new safe("
"); } - const username = Em.get(user, 'username'); + const username = Em.get(user, 'username'), + letterAvatarColor = Em.get(user, 'letter_avatar_color'); + if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); } - const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId); + const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId, letterAvatarColor); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); -}, 'username', 'uploaded_avatar_id', 'avatar_template'); +}, 'username', 'uploaded_avatar_id', 'letter_avatar_color', 'avatar_template'); /* * Used when we only have a template diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 index 1ab668ffc..aea2e9baa 100644 --- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 @@ -5,20 +5,20 @@ function renderAvatar(user, options) { options = options || {}; if (user) { - var username = Em.get(user, 'username'); + let username = Em.get(user, 'username'); if (!username) { if (!options.usernamePath) { return ''; } username = Em.get(user, options.usernamePath); } - var title; + let title; if (!options.ignoreTitle) { // first try to get a title title = Em.get(user, 'title'); // if there was no title provided if (!title) { // try to retrieve a description - var description = Em.get(user, 'description'); + const description = Em.get(user, 'description'); // if a description has been provided if (description && description.length > 0) { // preprend the username before the description @@ -28,13 +28,14 @@ function renderAvatar(user, options) { } // this is simply done to ensure we cache images correctly - var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'); + const uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'), + letterAvatarColor = Em.get(user, 'letter_avatar_color') || Em.get(user, 'user.letter_avatar_color'); return Discourse.Utilities.avatarImg({ size: options.imageSize, extraClasses: Em.get(user, 'extras') || options.extraClasses, title: title || username, - avatarTemplate: avatarTemplate(username, uploadedAvatarId) + avatarTemplate: Em.get("avatar_template") || avatarTemplate(username, uploadedAvatarId, letterAvatarColor) }); } else { return ''; diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 index 542e97959..731a2047d 100644 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 @@ -2,8 +2,10 @@ import { hashString } from 'discourse/lib/hash'; let _splitAvatars; -function defaultAvatar(username) { - const defaultAvatars = Discourse.SiteSettings.default_avatars; +function defaultAvatar(username, letterAvatarColor) { + const defaultAvatars = Discourse.SiteSettings.default_avatars, + version = Discourse.LetterAvatarVersion; + if (defaultAvatars && defaultAvatars.length) { _splitAvatars = _splitAvatars || defaultAvatars.split("\n"); @@ -13,20 +15,17 @@ function defaultAvatar(username) { } } - return Discourse.getURLWithCDN("/letter_avatar/" + - username.toLowerCase() + - "/{size}/" + - Discourse.LetterAvatarVersion + ".png"); + if (Discourse.SiteSettings.external_letter_avatars_enabled) { + const url = Discourse.SiteSettings.external_letter_avatars_url; + return `${url}/letter/${username[0]}?color=${letterAvatarColor}&size={size}`; + } else { + return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${version}.png`); + } } -export default function(username, uploadedAvatarId) { +export default function(username, uploadedAvatarId, letterAvatarColor) { if (uploadedAvatarId) { - return Discourse.getURLWithCDN("/user_avatar/" + - Discourse.BaseUrl + - "/" + - username.toLowerCase() + - "/{size}/" + - uploadedAvatarId + ".png"); + return Discourse.getURLWithCDN(`/user_avatar/${Discourse.BaseUrl}/${username.toLowerCase()}/{size}/${uploadedAvatarId}.png`); } - return defaultAvatar(username); + return defaultAvatar(username, letterAvatarColor); } diff --git a/app/assets/javascripts/discourse/models/user-action.js.es6 b/app/assets/javascripts/discourse/models/user-action.js.es6 index 2d273c43e..05e1e4929 100644 --- a/app/assets/javascripts/discourse/models/user-action.js.es6 +++ b/app/assets/javascripts/discourse/models/user-action.js.es6 @@ -1,5 +1,7 @@ import RestModel from 'discourse/models/rest'; import { url } from 'discourse/lib/computed'; +import { on } from 'ember-addons/ember-computed-decorators'; +import computed from 'ember-addons/ember-computed-decorators'; const UserActionTypes = { likes_given: 1, @@ -17,21 +19,22 @@ const UserActionTypes = { }; const InvertedActionTypes = {}; -_.each(UserActionTypes, function (k, v) { +_.each(UserActionTypes, (k, v) => { InvertedActionTypes[k] = v; }); const UserAction = RestModel.extend({ - _attachCategory: function() { + @on("init") + _attachCategory() { const categoryId = this.get('category_id'); if (categoryId) { this.set('category', Discourse.Category.findById(categoryId)); } - }.on('init'), + }, - descriptionKey: function() { - const action = this.get('action_type'); + @computed("action_type") + descriptionKey(action) { if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) { if (this.get('isPM')) { return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user'; @@ -59,34 +62,39 @@ const UserAction = RestModel.extend({ return this.get('targetUser') ? 'user_mentioned_you' : 'user_mentioned_user'; } } - }.property('action_type'), + }, - sameUser: function() { - return this.get('username') === Discourse.User.currentProp('username'); - }.property('username'), + @computed("username") + sameUser(username) { + return username === Discourse.User.currentProp('username'); + }, - targetUser: function() { - return this.get('target_username') === Discourse.User.currentProp('username'); - }.property('target_username'), + @computed("target_username") + targetUser(targetUsername) { + return targetUsername === Discourse.User.currentProp('username'); + }, presentName: Em.computed.any('name', 'username'), targetDisplayName: Em.computed.any('target_name', 'target_username'), actingDisplayName: Em.computed.any('acting_name', 'acting_username'), targetUserUrl: url('target_username', '/users/%@'), - usernameLower: function() { - return this.get('username').toLowerCase(); - }.property('username'), + @computed("username") + usernameLower(username) { + return username.toLowerCase(); + }, userUrl: url('usernameLower', '/users/%@'), - postUrl: function() { + @computed() + postUrl() { return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number')); - }.property(), + }, - replyUrl: function() { + @computed() + replyUrl() { return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number')); - }.property(), + }, replyType: Em.computed.equal('action_type', UserActionTypes.replies), postType: Em.computed.equal('action_type', UserActionTypes.posts), @@ -99,7 +107,7 @@ const UserAction = RestModel.extend({ postReplyType: Em.computed.or('postType', 'replyType'), removableBookmark: Em.computed.and('bookmarkType', 'sameUser'), - addChild: function(action) { + addChild(action) { let groups = this.get("childGroups"); if (!groups) { groups = { @@ -143,22 +151,23 @@ const UserAction = RestModel.extend({ "childGroups.edits.items", "childGroups.edits.items.@each", "childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"), - switchToActing: function() { + switchToActing() { this.setProperties({ username: this.get('acting_username'), uploaded_avatar_id: this.get('acting_uploaded_avatar_id'), + letter_avatar_color: this.get('action_letter_avatar_color'), name: this.get('actingDisplayName') }); } }); UserAction.reopenClass({ - collapseStream: function(stream) { + collapseStream(stream) { const uniq = {}; const collapsed = []; let pos = 0; - stream.forEach(function(item) { + stream.forEach(item => { const key = "" + item.topic_id + "-" + item.post_number; const found = uniq[key]; if (found === void 0) { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 01aef870a..8fac2812e 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -6,6 +6,7 @@ import UserPostsStream from 'discourse/models/user-posts-stream'; import Singleton from 'discourse/mixins/singleton'; import { longDate } from 'discourse/lib/formatter'; import computed from 'ember-addons/ember-computed-decorators'; +import { observes } from 'ember-addons/ember-computed-decorators'; import Badge from 'discourse/models/badge'; import UserBadge from 'discourse/models/user-badge'; @@ -18,13 +19,15 @@ const User = RestModel.extend({ hasNotPosted: Em.computed.not("hasPosted"), canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"), - stream: function() { + @computed() + stream() { return UserStream.create({ user: this }); - }.property(), + }, - postsStream: function() { + @computed() + postsStream() { return UserPostsStream.create({ user: this }); - }.property(), + }, staff: Em.computed.or('admin', 'moderator'), @@ -32,27 +35,22 @@ const User = RestModel.extend({ return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'}); }, - searchContext: function() { + @computed("username_lower") + searchContext(username) { return { type: 'user', - id: this.get('username_lower'), + id: username, user: this }; - }.property('username_lower'), + }, - /** - This user's display name. Returns the name if possible, otherwise returns the - username. - - @property displayName - @type {String} - **/ - displayName: function() { - if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) { - return this.get('name'); + @computed("username", "name") + displayName(username, name) { + if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) { + return name; } - return this.get('username'); - }.property('username', 'name'), + return username; + }, @computed('profile_background') profileBackground(bgUrl) { @@ -60,38 +58,23 @@ const User = RestModel.extend({ return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe(); }, - path: function(){ - return Discourse.getURL('/users/' + this.get('username_lower')); + @computed() + path() { // no need to observe, requires a hard refresh to update - }.property(), + return Discourse.getURL(`/users/${this.get('username_lower')}`); + }, - /** - Path to this user's administration - - @property adminPath - @type {String} - **/ adminPath: url('username_lower', "/admin/users/%@"), - /** - This user's username in lowercase. + @computed("username") + username_lower(username) { + return username.toLowerCase(); + }, - @property username_lower - @type {String} - **/ - username_lower: function() { - return this.get('username').toLowerCase(); - }.property('username'), - - /** - This user's trust level. - - @property trustLevel - @type {Integer} - **/ - trustLevel: function() { - return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(this.get('trust_level'), 10)); - }.property('trust_level'), + @computed("trust_level") + trustLevel(trustLevel) { + return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(trustLevel, 10)); + }, isBasic: Em.computed.equal('trust_level', 0), isLeader: Em.computed.equal('trust_level', 3), @@ -100,61 +83,36 @@ const User = RestModel.extend({ isSuspended: Em.computed.equal('suspended', true), - suspended: function() { - return this.get('suspended_till') && moment(this.get('suspended_till')).isAfter(); - }.property('suspended_till'), + @computed("suspended_till") + suspended(suspendedTill) { + return suspendedTill && moment(suspendedTill).isAfter(); + }, - suspendedTillDate: function() { - return longDate(this.get('suspended_till')); - }.property('suspended_till'), + @computed("suspended_till") + suspendedTillDate(suspendedTill) { + return longDate(suspendedTill); + }, - /** - Changes this user's username. - - @method changeUsername - @param {String} newUsername The user's new username - @returns Result of ajax call - **/ - changeUsername: function(newUsername) { - return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", { + changeUsername(new_username) { + return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, { type: 'PUT', - data: { new_username: newUsername } + data: { new_username } }); }, - /** - Changes this user's email address. - - @method changeEmail - @param {String} email The user's new email address\ - @returns Result of ajax call - **/ - changeEmail: function(email) { - return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", { + changeEmail(email) { + return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, { type: 'PUT', - data: { email: email } + data: { email } }); }, - /** - Returns a copy of this user. - - @method copy - @returns {User} - **/ - copy: function() { + copy() { return Discourse.User.create(this.getProperties(Ember.keys(this))); }, - /** - Save's this user's properties over AJAX via a PUT request. - - @method save - @returns {Promise} the result of the operation - **/ - save: function() { - const self = this, - data = this.getProperties( + save() { + const data = this.getProperties( 'auto_track_topics_after_msecs', 'bio_raw', 'website', @@ -179,10 +137,10 @@ const User = RestModel.extend({ 'card_background' ); - ['muted','watched','tracked'].forEach(function(s){ - var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')}); + ['muted','watched','tracked'].forEach(s => { + let cats = this.get(s + 'Categories').map(c => c.get('id')); // HACK: denote lack of categories - if(cats.length === 0) { cats = [-1]; } + if (cats.length === 0) { cats = [-1]; } data[s + '_category_ids'] = cats; }); @@ -192,26 +150,19 @@ const User = RestModel.extend({ // TODO: We can remove this when migrated fully to rest model. this.set('isSaving', true); - return Discourse.ajax("/users/" + this.get('username_lower'), { + return Discourse.ajax(`/users/${this.get('username_lower')}`, { data: data, type: 'PUT' - }).then(function(result) { - self.set('bio_excerpt', result.user.bio_excerpt); - - const userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + }).then(result => { + this.set('bio_excerpt', result.user.bio_excerpt); + const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); }).finally(() => { this.set('isSaving', false); }); }, - /** - Changes the password and calls the callback function on AJAX.complete. - - @method changePassword - @returns {Promise} the result of the change password operation - **/ - changePassword: function() { + changePassword() { return Discourse.ajax("/session/forgot_password", { dataType: 'json', data: { login: this.get('username') }, @@ -219,73 +170,63 @@ const User = RestModel.extend({ }); }, - /** - Loads a single user action by id. - - @method loadUserAction - @param {Integer} id The id of the user action being loaded - @returns A stream of the user's actions containing the action of id - **/ - loadUserAction: function(id) { - var self = this, - stream = this.get('stream'); - return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) { + loadUserAction(id) { + const stream = this.get('stream'); + return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => { if (result && result.user_action) { - var ua = result.user_action; + const ua = result.user_action; - if ((self.get('stream.filter') || ua.action_type) !== ua.action_type) return; - if (!self.get('stream.filter') && !self.inAllStream(ua)) return; + if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return; + if (!this.get('stream.filter') && !this.inAllStream(ua)) return; - var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]); + const action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]); stream.set('itemsLoaded', stream.get('itemsLoaded') + 1); stream.get('content').insertAt(0, action[0]); } }); }, - inAllStream: function(ua) { + inAllStream(ua) { return ua.action_type === Discourse.UserAction.TYPES.posts || ua.action_type === Discourse.UserAction.TYPES.topics; }, // The user's stat count, excluding PMs. - statsCountNonPM: function() { - var self = this; - + @computed("statsExcludingPms.@each.count") + statsCountNonPM() { if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0; - var count = 0; - _.each(this.get('statsExcludingPms'), function(val) { - if (self.inAllStream(val)){ + let count = 0; + _.each(this.get('statsExcludingPms'), val => { + if (this.inAllStream(val)) { count += val.count; } }); return count; - }.property('statsExcludingPms.@each.count'), + }, // The user's stats, excluding PMs. - statsExcludingPms: function() { + @computed("stats.@each.isPM") + statsExcludingPms() { if (Ember.isEmpty(this.get('stats'))) return []; return this.get('stats').rejectProperty('isPM'); - }.property('stats.@each.isPM'), + }, - findDetails: function(options) { - var user = this; + findDetails(options) { + const user = this; - return PreloadStore.getAndRemove("user_" + user.get('username'), function() { - return Discourse.ajax("/users/" + user.get('username') + '.json', {data: options}); - }).then(function (json) { + return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => { + return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options }); + }).then(json => { if (!Em.isEmpty(json.user.stats)) { - json.user.stats = Discourse.User.groupStats(_.map(json.user.stats,function(s) { + json.user.stats = Discourse.User.groupStats(_.map(json.user.stats, s => { if (s.count) s.count = parseInt(s.count, 10); return Discourse.UserActionStat.create(s); })); } if (!Em.isEmpty(json.user.custom_groups)) { - json.user.custom_groups = json.user.custom_groups.map(function (g) { - return Discourse.Group.create(g); - }); + json.user.custom_groups = json.user.custom_groups.map(g => Discourse.Group.create(g)); } if (json.user.invited_by) { @@ -294,12 +235,10 @@ const User = RestModel.extend({ if (!Em.isEmpty(json.user.featured_user_badge_ids)) { const userBadgesMap = {}; - UserBadge.createFromJson(json).forEach(function(userBadge) { + UserBadge.createFromJson(json).forEach(userBadge => { userBadgesMap[ userBadge.get('id') ] = userBadge; }); - json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) { - return userBadgesMap[id]; - }); + json.user.featured_user_badges = json.user.featured_user_badge_ids.map(id => userBadgesMap[id]); } if (json.user.card_badge) { @@ -311,30 +250,26 @@ const User = RestModel.extend({ }); }, - findStaffInfo: function() { + findStaffInfo() { if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); } - var self = this; - return Discourse.ajax("/users/" + this.get("username_lower") + "/staff-info.json").then(function(info) { - self.setProperties(info); + return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => { + this.setProperties(info); }); }, - avatarTemplate: function() { - return avatarTemplate(this.get('username'), this.get('uploaded_avatar_id')); - }.property('uploaded_avatar_id', 'username'), + @computed("username", "uploaded_avatar_id", "letter_avatar_color") + avatarTemplate(username, uploadedAvatarId, letterAvatarColor) { + return avatarTemplate(username, uploadedAvatarId, letterAvatarColor); + }, /* Change avatar selection */ - pickAvatar: function(uploadId) { - var self = this; - - return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", { + pickAvatar(uploadId) { + return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { type: 'PUT', data: { upload_id: uploadId } - }).then(function(){ - self.set('uploaded_avatar_id', uploadId); - }); + }).then(() => this.set('uploaded_avatar_id', uploadId)); }, /** @@ -344,7 +279,7 @@ const User = RestModel.extend({ @param {String} type The type of the upload (image, attachment) @returns true if the current user is allowed to upload a file **/ - isAllowedToUploadAFile: function(type) { + isAllowedToUploadAFile(type) { return this.get('staff') || this.get('trust_level') > 0 || Discourse.SiteSettings['newuser_max_' + type + 's'] > 0; @@ -357,35 +292,39 @@ const User = RestModel.extend({ @param {String} email The email address of the user to invite to the site @returns {Promise} the result of the server call **/ - createInvite: function(email, groupNames) { + createInvite(email, groupNames) { return Discourse.ajax('/invites', { type: 'POST', data: {email: email, group_names: groupNames} }); }, - generateInviteLink: function(email, groupNames, topicId) { + generateInviteLink(email, groupNames, topicId) { return Discourse.ajax('/invites/link', { type: 'POST', data: {email: email, group_names: groupNames, topic_id: topicId} }); }, - updateMutedCategories: function() { + @observes("muted_category_ids") + updateMutedCategories() { this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids)); - }.observes("muted_category_ids"), + }, - updateTrackedCategories: function() { + @observes("tracked_category_ids") + updateTrackedCategories() { this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids)); - }.observes("tracked_category_ids"), + }, - updateWatchedCategories: function() { + @observes("watched_category_ids") + updateWatchedCategories() { this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids)); - }.observes("watched_category_ids"), + }, - canDeleteAccount: function() { - return !Discourse.SiteSettings.enable_sso && this.get('can_delete_account') && ((this.get('reply_count')||0) + (this.get('topic_count')||0)) <= 1; - }.property('can_delete_account', 'reply_count', 'topic_count'), + @computed("can_delete_account", "reply_count", "topic_count") + canDeleteAccount(canDeleteAccount, replyCount, topicCount) { + return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1; + }, "delete": function() { if (this.get('can_delete_account')) { @@ -398,27 +337,26 @@ const User = RestModel.extend({ } }, - dismissBanner: function (bannerKey) { + dismissBanner(bannerKey) { this.set("dismissed_banner_key", bannerKey); - Discourse.ajax("/users/" + this.get('username'), { + Discourse.ajax(`/users/${this.get('username')}`, { type: 'PUT', data: { dismissed_banner_key: bannerKey } }); }, - checkEmail: function () { - var self = this; - return Discourse.ajax("/users/" + this.get("username_lower") + "/emails.json", { + checkEmail() { + return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, { type: "PUT", data: { context: window.location.pathname } - }).then(function (result) { + }).then(result => { if (result) { - self.setProperties({ + this.setProperties({ email: result.email, associated_accounts: result.associated_accounts }); } - }, function () {}); + }); } }); @@ -426,14 +364,14 @@ const User = RestModel.extend({ User.reopenClass(Singleton, { // Find a `Discourse.User` for a given username. - findByUsername: function(username, options) { + findByUsername(username, options) { const user = User.create({username: username}); return user.findDetails(options); }, // TODO: Use app.register and junk Singleton - createCurrent: function() { - var userJson = PreloadStore.get('currentUser'); + createCurrent() { + const userJson = PreloadStore.get('currentUser'); if (userJson) { const store = Discourse.__container__.lookup('store:main'); return store.createRecord('user', userJson); @@ -441,56 +379,38 @@ User.reopenClass(Singleton, { return null; }, - /** - Checks if given username is valid for this email address - - @method checkUsername - @param {String} username A username to check - @param {String} email An email address to check - @param {Number} forUserId user id - provide when changing username - **/ - checkUsername: function(username, email, forUserId) { + checkUsername(username, email, for_user_id) { return Discourse.ajax('/users/check_username', { - data: { username: username, email: email, for_user_id: forUserId } + data: { username, email, for_user_id } }); }, - /** - Groups the user's statistics - - @method groupStats - @param {Array} stats Given stats - @returns {Object} - **/ - groupStats: function(stats) { - var responses = Discourse.UserActionStat.create({ + groupStats(stats) { + const responses = Discourse.UserActionStat.create({ count: 0, action_type: Discourse.UserAction.TYPES.replies }); - stats.filterProperty('isResponse').forEach(function (stat) { + stats.filterProperty('isResponse').forEach(stat => { responses.set('count', responses.get('count') + stat.get('count')); }); - var result = Em.A(); + const result = Em.A(); result.pushObjects(stats.rejectProperty('isResponse')); - var insertAt = 0; - result.forEach(function(item, index){ - if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){ + let insertAt = 0; + result.forEach((item, index) => { + if (item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts) { insertAt = index + 1; } }); - if(responses.count > 0) { + if (responses.count > 0) { result.insertAt(insertAt, responses); } - return(result); + return result; }, - /** - Creates a new account - **/ - createAccount: function(attrs) { + createAccount(attrs) { return Discourse.ajax("/users", { data: { name: attrs.accountName, diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index d96c6a55a..6964d102a 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -252,9 +252,12 @@ const ComposerView = Ember.View.extend(Ember.Evented, { const quotedPost = posts.findProperty("post_number", postNumber); if (quotedPost) { const username = quotedPost.get('username'), - uploadId = quotedPost.get('uploaded_avatar_id'); + uploadId = quotedPost.get('uploaded_avatar_id'), + letterAvatarColor = quotedPost.get("letter_avatar_color"); - return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId)); + debugger; + + return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId, letterAvatarColor)); } } } diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d537c6b7a..cf72afcf6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -518,7 +518,7 @@ class UsersController < ApplicationController user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id] user_fields << :name if SiteSetting.enable_names? - to_render = { users: results.as_json(only: user_fields, methods: :avatar_template) } + to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template, :letter_avatar_color]) } if params[:include_groups] == "true" to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} } diff --git a/app/models/user.rb b/app/models/user.rb index 2861fff88..aa46dd7c7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -457,11 +457,11 @@ class User < ActiveRecord::Base avatar_template = split_avatars[hash.abs % split_avatars.size] end else - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + letter_avatar_template(username) end end - def self.avatar_template(username,uploaded_avatar_id) + def self.avatar_template(username, uploaded_avatar_id) return default_template(username) if !uploaded_avatar_id username ||= "" hostname = RailsMultisite::ConnectionManagement.current_hostname @@ -469,11 +469,26 @@ class User < ActiveRecord::Base end def self.letter_avatar_template(username) - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + if SiteSetting.external_letter_avatars_enabled + color = letter_avatar_color(username) + "#{SiteSetting.external_letter_avatars_url}/letter/#{username[0]}?color=#{color}&size={size}" + else + "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + end + end + + def letter_avatar_color + self.class.letter_avatar_color(username) + end + + def self.letter_avatar_color(username) + username = username || "" + color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length] + color.map { |c| c.to_s(16) }.join end def avatar_template - self.class.avatar_template(username,uploaded_avatar_id) + self.class.avatar_template(username, uploaded_avatar_id) end # The following count methods are somewhat slow - definitely don't use them in a loop. diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index 04b91ecad..4edb2b5cc 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -5,6 +5,7 @@ class BasicPostSerializer < ApplicationSerializer :username, :avatar_template, :uploaded_avatar_id, + :letter_avatar_color, :created_at, :cooked, :cooked_hidden @@ -25,9 +26,14 @@ class BasicPostSerializer < ApplicationSerializer object.user.try(:uploaded_avatar_id) end + def letter_avatar_color + object.user.try(:letter_avatar_color) + end + def cooked_hidden object.hidden && !scope.is_staff? end + def include_cooked_hidden? cooked_hidden end diff --git a/app/serializers/basic_user_serializer.rb b/app/serializers/basic_user_serializer.rb index 8911291f3..12ed3f363 100644 --- a/app/serializers/basic_user_serializer.rb +++ b/app/serializers/basic_user_serializer.rb @@ -1,5 +1,5 @@ class BasicUserSerializer < ApplicationSerializer - attributes :id, :username, :uploaded_avatar_id, :avatar_template + attributes :id, :username, :uploaded_avatar_id, :avatar_template, :letter_avatar_color def include_name? SiteSetting.enable_names? @@ -17,4 +17,12 @@ class BasicUserSerializer < ApplicationSerializer object[:user] || object end + def letter_avatar_color + if Hash === object + User.letter_avatar_color(user[:username]) + else + object.letter_avatar_color + end + end + end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 5ba620ca2..9bc469f04 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -178,7 +178,8 @@ class PostSerializer < BasicPostSerializer { username: object.reply_to_user.username, avatar_template: object.reply_to_user.avatar_template, - uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id + uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id, + letter_avatar_color: object.reply_to_user.letter_avatar_color, } end diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 8b3939963..4bf665004 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -27,9 +27,11 @@ class UserActionSerializer < ApplicationSerializer :edit_reason, :category_id, :uploaded_avatar_id, + :letter_avatar_color, :closed, :archived, - :acting_uploaded_avatar_id + :acting_uploaded_avatar_id, + :acting_letter_avatar_color def excerpt cooked = object.cooked || PrettyText.cook(object.raw) @@ -84,4 +86,12 @@ class UserActionSerializer < ApplicationSerializer object.topic_archived end + def letter_avatar_color + User.letter_avatar_color(username) + end + + def acting_letter_avatar_color + User.letter_avatar_color(acting_username) + end + end diff --git a/app/serializers/user_name_serializer.rb b/app/serializers/user_name_serializer.rb index ac7beaa8d..3d7fc0d1f 100644 --- a/app/serializers/user_name_serializer.rb +++ b/app/serializers/user_name_serializer.rb @@ -1,20 +1,3 @@ -class UserNameSerializer < ApplicationSerializer - attributes :id, :username, :name, :title, :uploaded_avatar_id, :avatar_template - - def include_name? - SiteSetting.enable_names? - end - - def avatar_template - if Hash === object - User.avatar_template(user[:username], user[:uploaded_avatar_id]) - else - object.avatar_template - end - end - - def user - object[:user] || object - end - +class UserNameSerializer < BasicUserSerializer + attributes :name, :title end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 895453990..2d216a515 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -323,4 +323,5 @@ class UserSerializer < BasicUserSerializer def pending_count 0 end + end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fcd190dc8..bfab232cf 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -979,6 +979,9 @@ en: avatar_sizes: "List of automatically generated avatar sizes." + external_letter_avatars_enabled: "Use external letter avatars service." + external_letter_avatars_url: "URL of the external letter avatars service." + enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks." default_invitee_trust_level: "Default trust level (0-4) for invited users." diff --git a/config/site_settings.yml b/config/site_settings.yml index 754e067b8..add19d2db 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -572,6 +572,13 @@ files: avatar_sizes: default: '20|25|32|45|60|120' type: list + external_letter_avatars_enabled: + default: false + client: true + external_letter_avatars_url: + default: "https://avatars.discourse.org" + client: true + regex: '^https?:\/\/.+[^\/]$' trust: default_trust_level: