mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 05:37:35 +02:00
feat: allow 3 profile pics (#14092)
* feat: allow 3 profile pics * test: fix notification test * test: fix user picture test * test: relative_path fixes * fix: relative_path getting saved in when updating profile pic
This commit is contained in:
@@ -27,7 +27,12 @@ define('accounts/picture', [
|
|||||||
icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] },
|
icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] },
|
||||||
defaultAvatar: ajaxify.data.defaultAvatar,
|
defaultAvatar: ajaxify.data.defaultAvatar,
|
||||||
allowProfileImageUploads: ajaxify.data.allowProfileImageUploads,
|
allowProfileImageUploads: ajaxify.data.allowProfileImageUploads,
|
||||||
iconBackgrounds: ajaxify.data.iconBackgrounds,
|
iconBackgrounds: ajaxify.data.iconBackgrounds.map((color) => {
|
||||||
|
return {
|
||||||
|
color,
|
||||||
|
selected: color === ajaxify.data['icon:bgColor'],
|
||||||
|
};
|
||||||
|
}),
|
||||||
user: {
|
user: {
|
||||||
uid: ajaxify.data.uid,
|
uid: ajaxify.data.uid,
|
||||||
username: ajaxify.data.username,
|
username: ajaxify.data.username,
|
||||||
@@ -55,9 +60,8 @@ define('accounts/picture', [
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.on('shown.bs.modal', updateImages);
|
modal.on('click', '[component="profile/picture/button"]', function selectImageType() {
|
||||||
modal.on('click', '.list-group-item', function selectImageType() {
|
modal.find('[component="profile/picture/button"]').removeClass('active');
|
||||||
modal.find('.list-group-item').removeClass('active');
|
|
||||||
$(this).addClass('active');
|
$(this).addClass('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,34 +73,17 @@ define('accounts/picture', [
|
|||||||
|
|
||||||
handleImageUpload(modal);
|
handleImageUpload(modal);
|
||||||
|
|
||||||
function updateImages() {
|
|
||||||
// Check to see which one is the active picture
|
|
||||||
if (!ajaxify.data.picture) {
|
|
||||||
modal.find('[data-type="default"]').addClass('active');
|
|
||||||
} else {
|
|
||||||
modal.find('.list-group-item img').each(function () {
|
|
||||||
if (this.getAttribute('src') === ajaxify.data.picture) {
|
|
||||||
$(this).parents('.list-group-item').addClass('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update avatar background colour
|
|
||||||
const iconbgEl = modal.find(`[data-bg-color="${ajaxify.data['icon:bgColor']}"]`);
|
|
||||||
if (iconbgEl.length) {
|
|
||||||
iconbgEl.addClass('selected');
|
|
||||||
} else {
|
|
||||||
modal.find('[data-bg-color="transparent"]').addClass('selected');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSelection() {
|
function saveSelection() {
|
||||||
const type = modal.find('.list-group-item.active').attr('data-type');
|
const activeBtn = modal.find('[component="profile/picture/button"].active');
|
||||||
|
const type = activeBtn.attr('data-type');
|
||||||
|
const picture = activeBtn.find('img').attr('src');
|
||||||
const iconBgColor = modal.find('[data-bg-color].selected').attr('data-bg-color') || 'transparent';
|
const iconBgColor = modal.find('[data-bg-color].selected').attr('data-bg-color') || 'transparent';
|
||||||
|
|
||||||
changeUserPicture(type, iconBgColor).then(() => {
|
api.put(`/users/${ajaxify.data.theirid}/picture`, {
|
||||||
|
type, picture, iconBgColor,
|
||||||
|
}).then(() => {
|
||||||
Picture.updateHeader(
|
Picture.updateHeader(
|
||||||
type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src'),
|
type === 'default' ? '' : picture,
|
||||||
iconBgColor
|
iconBgColor
|
||||||
);
|
);
|
||||||
ajaxify.refresh();
|
ajaxify.refresh();
|
||||||
@@ -158,13 +145,6 @@ define('accounts/picture', [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRemoveComplete() {
|
|
||||||
if (ajaxify.data.uploadedpicture === ajaxify.data.picture) {
|
|
||||||
ajaxify.refresh();
|
|
||||||
Picture.updateHeader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.find('[data-action="upload"]').on('click', function () {
|
modal.find('[data-action="upload"]').on('click', function () {
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
|
|
||||||
@@ -217,21 +197,24 @@ define('accounts/picture', [
|
|||||||
});
|
});
|
||||||
|
|
||||||
modal.find('[data-action="remove-uploaded"]').on('click', function () {
|
modal.find('[data-action="remove-uploaded"]').on('click', function () {
|
||||||
|
const removeBtn = $(this);
|
||||||
|
const removePicture = removeBtn.attr('data-url');
|
||||||
socket.emit('user.removeUploadedPicture', {
|
socket.emit('user.removeUploadedPicture', {
|
||||||
uid: ajaxify.data.theirid,
|
uid: ajaxify.data.theirid,
|
||||||
|
picture: removePicture,
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
modal.modal('hide');
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return alerts.error(err);
|
return alerts.error(err);
|
||||||
}
|
}
|
||||||
onRemoveComplete();
|
removeBtn.parent().remove();
|
||||||
|
if (removePicture === ajaxify.data.picture) {
|
||||||
|
modal.modal('hide');
|
||||||
|
ajaxify.refresh();
|
||||||
|
Picture.updateHeader();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeUserPicture(type, bgColor) {
|
|
||||||
return api.put(`/users/${ajaxify.data.theirid}/picture`, { type, bgColor });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Picture;
|
return Picture;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
|
const nconf = require('nconf');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
|
|
||||||
@@ -627,7 +627,14 @@ usersAPI.changePicture = async (caller, data) => {
|
|||||||
if (type === 'default') {
|
if (type === 'default') {
|
||||||
picture = '';
|
picture = '';
|
||||||
} else if (type === 'uploaded') {
|
} else if (type === 'uploaded') {
|
||||||
picture = await user.getUserField(data.uid, 'uploadedpicture');
|
const cleanPath = data.picture.replace(new RegExp(`^${nconf.get('relative_path')}`), '');
|
||||||
|
const isUserPicture = await user.isUserUploadedPicture(data.uid, cleanPath);
|
||||||
|
if (isUserPicture) {
|
||||||
|
await user.setUserField(data.uid, 'uploadedpicture', cleanPath);
|
||||||
|
picture = cleanPath;
|
||||||
|
} else {
|
||||||
|
picture = '';
|
||||||
|
}
|
||||||
} else if (type === 'external' && url) {
|
} else if (type === 'external' && url) {
|
||||||
picture = validator.escape(url);
|
picture = validator.escape(url);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const validator = require('validator');
|
||||||
|
const nconf = require('nconf');
|
||||||
|
|
||||||
|
const db = require('../../database');
|
||||||
const user = require('../../user');
|
const user = require('../../user');
|
||||||
const plugins = require('../../plugins');
|
const plugins = require('../../plugins');
|
||||||
|
|
||||||
@@ -10,7 +14,7 @@ module.exports = function (SocketUser) {
|
|||||||
}
|
}
|
||||||
await user.isAdminOrSelf(socket.uid, data.uid);
|
await user.isAdminOrSelf(socket.uid, data.uid);
|
||||||
// 'keepAllUserImages' is ignored, since there is explicit user intent
|
// 'keepAllUserImages' is ignored, since there is explicit user intent
|
||||||
const userData = await user.removeProfileImage(data.uid);
|
const userData = await user.removeProfileImage(data.uid, data.picture);
|
||||||
plugins.hooks.fire('action:user.removeUploadedPicture', {
|
plugins.hooks.fire('action:user.removeUploadedPicture', {
|
||||||
callerUid: socket.uid,
|
callerUid: socket.uid,
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
@@ -23,27 +27,29 @@ module.exports = function (SocketUser) {
|
|||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [list, userObj] = await Promise.all([
|
const [list, userObj, userPictures] = await Promise.all([
|
||||||
plugins.hooks.fire('filter:user.listPictures', {
|
plugins.hooks.fire('filter:user.listPictures', {
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
pictures: [],
|
pictures: [],
|
||||||
}),
|
}),
|
||||||
user.getUserData(data.uid),
|
user.getUserData(data.uid),
|
||||||
|
db.getSortedSetRevRange(`uid:${data.uid}:profile:pictures`, 0, 2),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (userObj.uploadedpicture) {
|
userPictures.forEach((picture) => {
|
||||||
list.pictures.push({
|
list.pictures.push({
|
||||||
type: 'uploaded',
|
type: 'uploaded',
|
||||||
url: userObj.uploadedpicture,
|
url: `${nconf.get('relative_path')}${picture}`,
|
||||||
text: '[[user:uploaded-picture]]',
|
text: '[[user:uploaded-picture]]',
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
// Normalize list into "user object" format
|
// Normalize list into "user object" format
|
||||||
list.pictures = list.pictures.map(({ type, url, text }) => ({
|
list.pictures = list.pictures.map(({ type, url, text }) => ({
|
||||||
type,
|
type,
|
||||||
username: text,
|
username: text,
|
||||||
picture: url,
|
picture: validator.escape(String(url)),
|
||||||
|
selected: url === userObj.picture,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
list.pictures.unshift({
|
list.pictures.unshift({
|
||||||
@@ -51,6 +57,7 @@ module.exports = function (SocketUser) {
|
|||||||
'icon:text': userObj['icon:text'],
|
'icon:text': userObj['icon:text'],
|
||||||
'icon:bgColor': userObj['icon:bgColor'],
|
'icon:bgColor': userObj['icon:bgColor'],
|
||||||
username: '[[user:default-picture]]',
|
username: '[[user:default-picture]]',
|
||||||
|
selected: !userObj.picture,
|
||||||
});
|
});
|
||||||
|
|
||||||
return list.pictures;
|
return list.pictures;
|
||||||
|
|||||||
23
src/upgrades/4.10.0/user-profile-pictures-zset.js
Normal file
23
src/upgrades/4.10.0/user-profile-pictures-zset.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const db = require('../../database');
|
||||||
|
const batch = require('../../batch');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'Add uid:<uid>:profile:pictures zset',
|
||||||
|
timestamp: Date.UTC(2026, 2, 13),
|
||||||
|
method: async function () {
|
||||||
|
const { progress } = this;
|
||||||
|
await batch.processSortedSet('users:joindate', async (uids) => {
|
||||||
|
const userData = await db.getObjects(uids.map(uid => `user:${uid}`));
|
||||||
|
const now = Date.now();
|
||||||
|
const bulkAdd = userData.filter(u => u && u.uploadedpicture)
|
||||||
|
.map(u => ([`uid:${u.uid}:profile:pictures`, now, u.uploadedpicture]));
|
||||||
|
await db.sortedSetAddBulk(bulkAdd);
|
||||||
|
progress.incr(uids.length);
|
||||||
|
}, {
|
||||||
|
batch: 500,
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -376,7 +376,8 @@ module.exports = function (User) {
|
|||||||
const _iconBackgrounds = [
|
const _iconBackgrounds = [
|
||||||
'#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3',
|
'#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3',
|
||||||
'#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722',
|
'#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722',
|
||||||
'#795548', '#607d8b',
|
'#795548', '#607d8b', '#00bcd4', '#ffc107', '#8bc34a', '#9e9e9e',
|
||||||
|
'#004d40', '#ad1457',
|
||||||
];
|
];
|
||||||
|
|
||||||
const data = await plugins.hooks.fire('filter:user.iconBackgrounds', { iconBackgrounds: _iconBackgrounds });
|
const data = await plugins.hooks.fire('filter:user.iconBackgrounds', { iconBackgrounds: _iconBackgrounds });
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ module.exports = function (User) {
|
|||||||
`uid:${uid}:flag:pids`,
|
`uid:${uid}:flag:pids`,
|
||||||
`uid:${uid}:sessions`,
|
`uid:${uid}:sessions`,
|
||||||
`uid:${uid}:shares`,
|
`uid:${uid}:shares`,
|
||||||
|
`uid:${uid}:profile:images`,
|
||||||
`invitation:uid:${uid}`,
|
`invitation:uid:${uid}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ module.exports = function (User) {
|
|||||||
const filename = `${data.uid}-profilecover-${Date.now()}${extension}`;
|
const filename = `${data.uid}-profilecover-${Date.now()}${extension}`;
|
||||||
const uploadData = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture);
|
const uploadData = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture);
|
||||||
|
|
||||||
await deleteCurrentPicture(data.uid, 'cover:url');
|
if (!meta.config['profile:keepAllUserImages']) {
|
||||||
|
await deletePicture(data.uid, 'cover:url');
|
||||||
|
}
|
||||||
|
|
||||||
await User.setUserField(data.uid, 'cover:url', uploadData.url);
|
await User.setUserField(data.uid, 'cover:url', uploadData.url);
|
||||||
|
|
||||||
if (data.position) {
|
if (data.position) {
|
||||||
@@ -87,30 +90,11 @@ module.exports = function (User) {
|
|||||||
throw new Error('[[error:invalid-image-extension]]');
|
throw new Error('[[error:invalid-image-extension]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedPath = await convertToPNG(userPhoto.path);
|
return await storeUserUploadedPicture(data.callerUid, data.uid, {
|
||||||
const isNormalized = userPhoto.path !== normalizedPath;
|
path: userPhoto.path,
|
||||||
|
type: userPhoto.type,
|
||||||
await image.resizeImage({
|
extension,
|
||||||
path: normalizedPath,
|
|
||||||
type: isNormalized ? 'image/png' : userPhoto.type,
|
|
||||||
width: meta.config.profileImageDimension,
|
|
||||||
height: meta.config.profileImageDimension,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filename = generateProfileImageFilename(data.uid, extension);
|
|
||||||
const uploadedImage = await image.uploadImage(filename, `profile/uid-${data.uid}`, {
|
|
||||||
uid: data.uid,
|
|
||||||
path: normalizedPath,
|
|
||||||
name: 'profileAvatar',
|
|
||||||
});
|
|
||||||
|
|
||||||
await deleteCurrentPicture(data.uid, 'uploadedpicture');
|
|
||||||
await User.updateProfile(data.callerUid, {
|
|
||||||
uid: data.uid,
|
|
||||||
uploadedpicture: uploadedImage.url,
|
|
||||||
picture: uploadedImage.url,
|
|
||||||
}, ['uploadedpicture', 'picture']);
|
|
||||||
return uploadedImage;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// uploads image data in base64 as profile picture
|
// uploads image data in base64 as profile picture
|
||||||
@@ -133,40 +117,67 @@ module.exports = function (User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
picture.path = await image.writeImageDataToTempFile(data.imageData);
|
picture.path = await image.writeImageDataToTempFile(data.imageData);
|
||||||
const normalizedPath = await convertToPNG(picture.path);
|
|
||||||
const isNormalized = picture.path !== normalizedPath;
|
return await storeUserUploadedPicture(data.callerUid, data.uid, {
|
||||||
picture.path = normalizedPath;
|
|
||||||
await image.resizeImage({
|
|
||||||
path: picture.path,
|
path: picture.path,
|
||||||
type: isNormalized ? 'image/png' : type,
|
type,
|
||||||
width: meta.config.profileImageDimension,
|
extension,
|
||||||
height: meta.config.profileImageDimension,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filename = generateProfileImageFilename(data.uid, extension);
|
|
||||||
const uploadedImage = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture);
|
|
||||||
|
|
||||||
await deleteCurrentPicture(data.uid, 'uploadedpicture');
|
|
||||||
await User.updateProfile(data.callerUid, {
|
|
||||||
uid: data.uid,
|
|
||||||
uploadedpicture: uploadedImage.url,
|
|
||||||
picture: uploadedImage.url,
|
|
||||||
}, ['uploadedpicture', 'picture']);
|
|
||||||
return uploadedImage;
|
|
||||||
} finally {
|
} finally {
|
||||||
await file.delete(picture.path);
|
await file.delete(picture.path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function deleteCurrentPicture(uid, field) {
|
async function storeUserUploadedPicture(callerUid, updateUid, picture) {
|
||||||
if (meta.config['profile:keepAllUserImages']) {
|
const { type, extension } = picture;
|
||||||
return;
|
const normalizedPath = await convertToPNG(picture.path);
|
||||||
|
const isNormalized = picture.path !== normalizedPath;
|
||||||
|
|
||||||
|
await image.resizeImage({
|
||||||
|
path: normalizedPath,
|
||||||
|
type: isNormalized ? 'image/png' : type,
|
||||||
|
width: meta.config.profileImageDimension,
|
||||||
|
height: meta.config.profileImageDimension,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filename = generateProfileImageFilename(updateUid, extension);
|
||||||
|
const uploadedImage = await image.uploadImage(filename, `profile/uid-${updateUid}`, {
|
||||||
|
uid: updateUid,
|
||||||
|
path: picture.path,
|
||||||
|
name: 'profileAvatar',
|
||||||
|
});
|
||||||
|
|
||||||
|
await User.updateProfile(callerUid, {
|
||||||
|
uid: updateUid,
|
||||||
|
uploadedpicture: uploadedImage.url,
|
||||||
|
picture: uploadedImage.url,
|
||||||
|
}, ['uploadedpicture', 'picture']);
|
||||||
|
|
||||||
|
const zsetKey = `uid:${updateUid}:profile:pictures`;
|
||||||
|
|
||||||
|
if (!meta.config['profile:keepAllUserImages']) {
|
||||||
|
// if we are not keeping all images, only keep most recent 3
|
||||||
|
const imagesToKeep = 3;
|
||||||
|
const previousImages = await db.getSortedSetRevRangeWithScores(zsetKey, 0, -1);
|
||||||
|
const toDeleteImages = previousImages.filter((imagePath, index) => index >= imagesToKeep - 1)
|
||||||
|
.map(image => image.value);
|
||||||
|
const toRemove = [
|
||||||
|
...toDeleteImages.map(imagePath => ([zsetKey, imagePath])),
|
||||||
|
];
|
||||||
|
|
||||||
|
await db.sortedSetRemoveBulk(toRemove);
|
||||||
|
toDeleteImages.forEach((imagePath) => {
|
||||||
|
if (imagePath && !imagePath.startsWith('http')) {
|
||||||
|
file.delete(imagePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await deletePicture(uid, field);
|
await db.sortedSetAdd(zsetKey, Date.now(), uploadedImage.url);
|
||||||
|
return { url: uploadedImage.url };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deletePicture(uid, field) {
|
async function deletePicture(uid, field) {
|
||||||
const uploadPath = await getPicturePath(uid, field);
|
const uploadPath = await getPicturePathFromUserField(uid, field);
|
||||||
if (uploadPath) {
|
if (uploadPath) {
|
||||||
await file.delete(uploadPath);
|
await file.delete(uploadPath);
|
||||||
}
|
}
|
||||||
@@ -207,31 +218,56 @@ module.exports = function (User) {
|
|||||||
await db.deleteObjectFields(`user:${data.uid}`, ['cover:url', 'cover:position']);
|
await db.deleteObjectFields(`user:${data.uid}`, ['cover:url', 'cover:position']);
|
||||||
};
|
};
|
||||||
|
|
||||||
User.removeProfileImage = async function (uid) {
|
// this function expects a path without nconf.get('relative_path) prepended
|
||||||
|
User.isUserUploadedPicture = async (uid, picture) => {
|
||||||
|
return await db.isSortedSetMember(`uid:${uid}:profile:pictures`, picture);
|
||||||
|
};
|
||||||
|
|
||||||
|
User.removeProfileImage = async function (uid, picture) {
|
||||||
const userData = await User.getUserFields(uid, ['uploadedpicture', 'picture']);
|
const userData = await User.getUserFields(uid, ['uploadedpicture', 'picture']);
|
||||||
await deletePicture(uid, 'uploadedpicture');
|
if (!picture) {
|
||||||
await User.setUserFields(uid, {
|
picture = userData.uploadedpicture;
|
||||||
uploadedpicture: '',
|
}
|
||||||
// if current picture is uploaded picture, reset to user icon
|
// picture has relative_path prepended, db entries don't have it, so remove it
|
||||||
picture: userData.uploadedpicture === userData.picture ? '' : userData.picture,
|
const cleanPath = picture.replace(new RegExp(`^${nconf.get('relative_path')}`), '');
|
||||||
});
|
const isUserPicture = await User.isUserUploadedPicture(uid, cleanPath);
|
||||||
|
if (isUserPicture) {
|
||||||
|
const path = getPicturePath(uid, picture);
|
||||||
|
await Promise.all([
|
||||||
|
path && !path.startsWith('http') ? file.delete(path) : null,
|
||||||
|
db.sortedSetRemove(`uid:${uid}:profile:pictures`, cleanPath),
|
||||||
|
]);
|
||||||
|
if (picture === userData.picture) {
|
||||||
|
// if deleting current uploaded picture, reset to user icon
|
||||||
|
await User.setUserFields(uid, {
|
||||||
|
uploadedpicture: '',
|
||||||
|
picture: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
};
|
};
|
||||||
|
|
||||||
User.getLocalCoverPath = async function (uid) {
|
User.getLocalCoverPath = async function (uid) {
|
||||||
return getPicturePath(uid, 'cover:url');
|
return await getPicturePathFromUserField(uid, 'cover:url');
|
||||||
};
|
};
|
||||||
|
|
||||||
User.getLocalAvatarPath = async function (uid) {
|
User.getLocalAvatarPath = async function (uid) {
|
||||||
return getPicturePath(uid, 'uploadedpicture');
|
return await getPicturePathFromUserField(uid, 'uploadedpicture');
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getPicturePath(uid, field) {
|
async function getPicturePathFromUserField(uid, field) {
|
||||||
const value = await User.getUserField(uid, field);
|
const value = await User.getUserField(uid, field);
|
||||||
|
return getPicturePath(uid, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPicturePath(uid, value) {
|
||||||
if (!value || !value.startsWith(`${nconf.get('relative_path')}/assets/uploads/profile/uid-${uid}`)) {
|
if (!value || !value.startsWith(`${nconf.get('relative_path')}/assets/uploads/profile/uid-${uid}`)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const filename = value.split('/').pop();
|
const filename = value.split('/').pop();
|
||||||
return path.join(nconf.get('upload_path'), `profile/uid-${uid}`, filename);
|
return path.join(nconf.get('upload_path'), `profile/uid-${uid}`, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,43 +1,39 @@
|
|||||||
<div class="row gy-2">
|
<div class="row gy-2">
|
||||||
<div class="col-12 col-sm-8 col-md-6">
|
<div class="col-12 col-sm-8 col-md-6">
|
||||||
<div class="list-group">
|
<div class="d-flex flex-column gap-2">
|
||||||
{{{each pictures}}}
|
{{{ each pictures }}}
|
||||||
<button type="button" class="list-group-item d-flex p-3" data-type="{pictures.type}">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<div class="flex-shrink-0">
|
<button component="profile/picture/button" type="button" class="btn btn-ghost border d-flex p-3 flex-grow-1 {{{ if ./selected }}}active{{{ end }}}" data-type="{./type}" data-url="{./picture}">
|
||||||
{buildAvatar(pictures, "48px", true)}
|
<div class="flex-shrink-0">
|
||||||
</div>
|
{buildAvatar(pictures, "48px", true)}
|
||||||
<div class="flex-grow-1 ms-3 align-self-center fs-5 text-start">
|
</div>
|
||||||
{pictures.username}
|
<div class="flex-grow-1 ms-3 align-self-center fs-5 text-start">
|
||||||
</div>
|
{./username}
|
||||||
</button>
|
</div>
|
||||||
{{{end}}}
|
</button>
|
||||||
|
<button class="btn btn-sm btn-ghost border {{{ if (./type != "uploaded") }}}invisible{{{ end }}}" data-action="remove-uploaded" data-url="{./picture}"><i class="text-danger fa-solid fa-trash-can"></i></button>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-4 col-md-6">
|
<div class="col-12 col-sm-4 col-md-6">
|
||||||
<div class="list-group">
|
<div class="d-flex flex-column gap-2">
|
||||||
|
<h5>[[user:avatar-background-colour]]</h5>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<a href="#" class="lh-1 p-1" data-bg-color="transparent"><i class="fa-solid fa-2x fa-ban text-secondary"></i></a>
|
||||||
|
{{{ each iconBackgrounds }}}
|
||||||
|
<a href="#" class="lh-1 p-1 {{{ if ./selected }}}selected{{{ end }}}" data-bg-color="{./color}" style="color: {./color};"><i class="fa-solid fa-2x fa-circle"></i></a>
|
||||||
|
{{{ end }}}
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
{{{ if allowProfileImageUploads }}}
|
{{{ if allowProfileImageUploads }}}
|
||||||
<button type="button" class="list-group-item" data-action="upload">
|
<button type="button" class="btn btn-ghost border" data-action="upload">
|
||||||
[[user:upload-new-picture]]
|
[[user:upload-new-picture]]
|
||||||
</button>
|
</button>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
<button type="button" class="list-group-item" data-action="upload-url">
|
<button type="button" class="btn btn-ghost border" data-action="upload-url">
|
||||||
[[user:upload-new-picture-from-url]]
|
[[user:upload-new-picture-from-url]]
|
||||||
</button>
|
</button>
|
||||||
{{{ if uploaded }}}
|
|
||||||
<button type="button" class="list-group-item" data-action="remove-uploaded">
|
|
||||||
[[user:remove-uploaded-picture]]
|
|
||||||
</button>
|
|
||||||
{{{ end }}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h4>[[user:avatar-background-colour]]</h4>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<a href="#" class="lh-1 p-1" data-bg-color="transparent"><i class="fa-solid fa-2x fa-ban text-secondary"></i></a>
|
|
||||||
{{{ each iconBackgrounds }}}
|
|
||||||
<a href="#" class="lh-1 p-1" data-bg-color="{@value}" style="color: {@value};"><i class="fa-solid fa-2x fa-circle"></i></a>
|
|
||||||
{{{ end }}}
|
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,8 @@ describe('Notifications', () => {
|
|||||||
const notifData = await notifications.get(nid);
|
const notifData = await notifications.get(nid);
|
||||||
assert.strictEqual(notifData.icon, undefined);
|
assert.strictEqual(notifData.icon, undefined);
|
||||||
assert.strictEqual(notifData.user['icon:text'], 'I');
|
assert.strictEqual(notifData.user['icon:text'], 'I');
|
||||||
assert.strictEqual(notifData.user['icon:bgColor'], '#3f51b5');
|
assert(notifData.user['icon:bgColor'].length === 7 &&
|
||||||
|
notifData.user['icon:bgColor'].startsWith('#'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null if pid is same and importance is lower', (done) => {
|
it('should return null if pid is same and importance is lower', (done) => {
|
||||||
|
|||||||
@@ -1028,7 +1028,8 @@ describe('User', () => {
|
|||||||
|
|
||||||
it('should set user picture to uploaded', async () => {
|
it('should set user picture to uploaded', async () => {
|
||||||
await User.setUserField(uid, 'uploadedpicture', '/test');
|
await User.setUserField(uid, 'uploadedpicture', '/test');
|
||||||
await apiUser.changePicture({ uid: uid }, { type: 'uploaded', uid: uid });
|
await db.sortedSetAdd(`uid:${uid}:profile:pictures`, Date.now(), '/test');
|
||||||
|
await apiUser.changePicture({ uid: uid }, { type: 'uploaded', picture: '/test', uid: uid });
|
||||||
const picture = await User.getUserField(uid, 'picture');
|
const picture = await User.getUserField(uid, 'picture');
|
||||||
assert.equal(picture, `${nconf.get('relative_path')}/test`);
|
assert.equal(picture, `${nconf.get('relative_path')}/test`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user