diff --git a/html/src/app.js b/html/src/app.js index 561f7b91..4905f94d 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -4045,6 +4045,7 @@ speechSynthesis.getVoices(); API.$on('LOGIN', function () { $app.localFavoriteFriends.clear(); + $app.currentUserGroupsInit = false; this.cachedFavorites.clear(); this.cachedFavoritesByObjectId.clear(); this.cachedFavoriteGroups.clear(); @@ -5036,8 +5037,19 @@ speechSynthesis.getVoices(); case 'group-member-updated': var groupId = content.member.groupId; - API.getGroup({ groupId, includeRoles: true }); + if ( + $app.groupDialog.visible && + $app.groupDialog.id === groupId + ) { + API.getGroup({ groupId, includeRoles: true }); + } $app.onGroupJoined(groupId); + this.$emit('GROUP:MEMBER', { + json: content.member, + params: { + groupId + } + }); console.log('group-member-updated', content); // content { @@ -6544,7 +6556,7 @@ speechSynthesis.getVoices(); imageUrl = noty.details.imageUrl; } else if (noty.imageUrl) { imageUrl = noty.imageUrl; - } else if (userId) { + } else if (userId && !userId.startsWith('grp_')) { imageUrl = await API.getCachedUser({ userId }) @@ -6590,7 +6602,7 @@ speechSynthesis.getVoices(); ); } } catch (err) { - console.error(err); + console.error(imageUrl, err); } return imageLocation; }; @@ -6711,6 +6723,9 @@ speechSynthesis.getVoices(); case 'group.joinRequest': this.speak(noty.message); break; + case 'group.transfer': + this.speak(noty.message); + break; case 'group.queueReady': this.speak(noty.message); break; @@ -6944,6 +6959,9 @@ speechSynthesis.getVoices(); case 'group.joinRequest': AppApi.XSNotification('VRCX', noty.message, timeout, image); break; + case 'group.transfer': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; case 'group.queueReady': AppApi.XSNotification('VRCX', noty.message, timeout, image); break; @@ -7306,6 +7324,16 @@ speechSynthesis.getVoices(); image ); break; + case 'group.transfer': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; case 'group.queueReady': AppApi.OVRTNotification( playOvrtHudNotifications, @@ -7642,6 +7670,13 @@ speechSynthesis.getVoices(); image ); break; + case 'group.transfer': + AppApi.DesktopNotification( + 'Group Transfer Request', + noty.message, + image + ); + break; case 'group.queueReady': AppApi.DesktopNotification( 'Instance Queue Ready', @@ -9919,11 +9954,16 @@ speechSynthesis.getVoices(); // eslint-disable-next-line require-atomic-updates $app.feedSessionTable = await database.getFeedDatabase(); $app.feedTableLookup(); + if (typeof args.json.presence?.groups !== 'undefined') { + await $app.loadCurrentUserGroups( + args.json.id, + args.json.presence.groups + ); + } + await $app.getCurrentUserGroups(); // eslint-disable-next-line require-atomic-updates $app.notificationTable.data = await database.getNotifications(); await this.refreshNotifications(); - await $app.loadCurrentUserGroups(); - await $app.getCurrentUserGroups(); try { if ( await configRepository.getBool(`friendLogInit_${args.json.id}`) @@ -15595,6 +15635,7 @@ speechSynthesis.getVoices(); 'group.informative': 'On', 'group.invite': 'On', 'group.joinRequest': 'Off', + 'group.transfer': 'On', 'group.queueReady': 'On', 'instance.closed': 'On', PortalSpawn: 'Everyone', @@ -15635,6 +15676,7 @@ speechSynthesis.getVoices(); 'group.informative': 'On', 'group.invite': 'On', 'group.joinRequest': 'On', + 'group.transfer': 'On', 'group.queueReady': 'On', 'instance.closed': 'On', PortalSpawn: 'Everyone', @@ -15698,6 +15740,10 @@ speechSynthesis.getVoices(); $app.data.sharedFeedFilters.noty.groupChange = 'On'; $app.data.sharedFeedFilters.wrist.groupChange = 'On'; } + if (!$app.data.sharedFeedFilters.noty['group.transfer']) { + $app.data.sharedFeedFilters.noty['group.transfer'] = 'On'; + $app.data.sharedFeedFilters.wrist['group.transfer'] = 'On'; + } $app.data.trustColor = JSON.parse( await configRepository.getString( @@ -24705,6 +24751,9 @@ speechSynthesis.getVoices(); params.frames = $app.emojiAnimFrameCount; params.framesOverTime = $app.emojiAnimFps; } + if ($app.emojiAnimLoopPingPong) { + params.loopStyle = 'pingpong'; + } var base64Body = btoa(r.result); API.uploadEmoji(base64Body, params).then((args) => { $app.$message({ @@ -24748,6 +24797,7 @@ speechSynthesis.getVoices(); $app.data.emojiAnimFrameCount = 4; $app.data.emojiAnimType = false; $app.data.emojiAnimationStyle = 'Stop'; + $app.data.emojiAnimLoopPingPong = false; $app.data.emojiAnimationStyleUrl = 'https://assets.vrchat.com/www/images/emoji-previews/'; $app.data.emojiAnimationStyleList = { @@ -28155,18 +28205,16 @@ speechSynthesis.getVoices(); $app.groupDialog.ref.myMember.isSubscribedToAnnouncements = json.isSubscribedToAnnouncements; } - delete json.visibility; if ( $app.userDialog.visible && $app.userDialog.id === this.currentUser.id ) { $app.getCurrentUserRepresentedGroup(); } - this.$emit('GROUP', { + this.$emit('GROUP:MEMBER', { json, params: { - groupId: json.groupId, - userId: args.params.userId + groupId: json.groupId } }); }); @@ -28338,10 +28386,142 @@ speechSynthesis.getVoices(); total = args.json.total; offset += n; } while (offset < total); - return { + var returnArgs = { posts, params }; + this.$emit('GROUP:POSTS:ALL', returnArgs); + return returnArgs; + }; + + API.$on('GROUP:POSTS:ALL', function (args) { + var D = $app.groupDialog; + if (D.id === args.params.groupId) { + for (var post of args.posts) { + post.title = $app.replaceBioSymbols(post.title); + post.text = $app.replaceBioSymbols(post.text); + } + if (args.posts.length > 0) { + D.announcement = args.posts[0]; + } + D.posts = args.posts; + $app.updateGroupPostSearch(); + } + }); + + API.$on('GROUP:POST', function (args) { + var D = $app.groupDialog; + if (D.id !== args.params.groupId) { + return; + } + + var newPost = args.json; + newPost.title = $app.replaceBioSymbols(newPost.title); + newPost.text = $app.replaceBioSymbols(newPost.text); + var hasPost = false; + // update existing post + for (var post of D.posts) { + if (post.id === newPost.id) { + Object.assign(post, newPost); + hasPost = true; + break; + } + } + // set or update announcement + if (newPost.id === D.announcement.id || !D.announcement.id) { + D.announcement = newPost; + } + // add new post + if (!hasPost) { + D.posts.unshift(newPost); + } + $app.updateGroupPostSearch(); + }); + + API.$on('GROUP:POST:DELETE', function (args) { + var D = $app.groupDialog; + if (D.id !== args.params.groupId) { + return; + } + + var postId = args.params.postId; + // remove existing post + for (var post of D.posts) { + if (post.id === postId) { + removeFromArray(D.posts, post); + break; + } + } + // remove/update announcement + if (postId === D.announcement.id) { + if (D.posts.length > 0) { + D.announcement = D.posts[0]; + } else { + D.announcement = {}; + } + } + $app.updateGroupPostSearch(); + }); + + $app.methods.confirmDeleteGroupPost = function (post) { + this.$confirm('Are you sure you want to delete this post?', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.deleteGroupPost({ + groupId: post.groupId, + postId: post.id + }); + } + } + }); + }; + + /** + * @param {{ groupId: string, postId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.deleteGroupPost = function (params) { + return this.call(`groups/${params.groupId}/posts/${params.postId}`, { + method: 'DELETE' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POST:DELETE', args); + return args; + }); + }; + + API.editGroupPost = function (params) { + return this.call(`groups/${params.groupId}/posts/${params.postId}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POST', args); + return args; + }); + }; + + API.createGroupPost = function (params) { + return this.call(`groups/${params.groupId}/posts`, { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POST', args); + return args; + }); }; /** @@ -28519,6 +28699,12 @@ speechSynthesis.getVoices(); }); }; + API.$on('GROUP:INSTANCES', function (args) { + if ($app.groupDialog.id === args.params.groupId) { + $app.applyGroupDialogInstances(args.json.instances); + } + }); + API.$on('GROUP:INSTANCES', function (args) { for (var json of args.json.instances) { this.$emit('INSTANCE', { @@ -28730,11 +28916,15 @@ speechSynthesis.getVoices(); } else { if (this.currentUserGroups.has(ref.id)) { // compare group props - if (ref.ownerId && ref.ownerId !== json.ownerId) { + if ( + ref.ownerId && + json.ownerId && + ref.ownerId !== json.ownerId + ) { // owner changed $app.groupOwnerChange(json, ref.ownerId, json.ownerId); } - if (ref.name && ref.name !== json.name) { + if (ref.name && json.name && ref.name !== json.name) { // name changed $app.groupChange( json, @@ -28818,12 +29008,19 @@ speechSynthesis.getVoices(); }; $app.methods.groupChange = function (ref, message) { + if (!this.currentUserGroupsInit) { + return; + } // oh the level of cursed for compibility var json = { id: Math.random().toString(36), type: 'groupChange', senderUserId: ref.id, senderUsername: ref.name, + imageUrl: ref.iconUrl, + details: { + imageUrl: ref.iconUrl + }, message, created_at: new Date().toJSON() }; @@ -28838,13 +29035,19 @@ speechSynthesis.getVoices(); workerTimers.setTimeout(this.saveCurrentUserGroups, 100); }; + $app.data.currentUserGroupsInit = false; + $app.methods.saveCurrentUserGroups = function () { + if (!this.currentUserGroupsInit) { + return; + } var groups = []; for (var ref of API.currentUserGroups.values()) { groups.push({ id: ref.id, name: ref.name, ownerId: ref.ownerId, + iconUrl: ref.iconUrl, roles: ref.roles, roleIds: ref.myMember?.roleIds }); @@ -28855,25 +29058,40 @@ speechSynthesis.getVoices(); ); }; - $app.methods.loadCurrentUserGroups = async function () { + $app.methods.loadCurrentUserGroups = async function (userId, groups) { if ( - !(await configRepository.getString( - `VRCX_currentUserGroups_${API.currentUser.id}` + !(await configRepository.getBool( + `VRCX_currentUserGroupsInit_${userId}` )) ) { + // REMOVE ME: clean up 18/04/2024 mess + await database.fixBrokenGroupChange(); + // fetch every group with roles for storing and comparing later - for (var group of API.currentUserGroups.values()) { - await API.getGroup({ - groupId: group.id, - includeRoles: true - }); + for (var i = 0; i < groups.length; i++) { + var groupId = groups[i]; + try { + var args = await API.getGroup({ + groupId, + includeRoles: true + }); + var ref = API.applyGroup(args.json); + API.currentUserGroups.set(groupId, ref); + } catch (err) { + console.error(err); + } } this.saveCurrentUserGroups(); + this.currentUserGroupsInit = true; + configRepository.setBool( + `VRCX_currentUserGroupsInit_${userId}`, + true + ); return; } var groups = JSON.parse( await configRepository.getString( - `VRCX_currentUserGroups_${API.currentUser.id}`, + `VRCX_currentUserGroups_${userId}`, '[]' ) ); @@ -28883,7 +29101,7 @@ speechSynthesis.getVoices(); var ref = { id: group.id, name: group.name, - iconUrl: '', + iconUrl: group.iconUrl, ownerId: group.ownerId, roles: group.roles, myMember: { @@ -28893,6 +29111,7 @@ speechSynthesis.getVoices(); API.cachedGroups.set(group.id, ref); API.currentUserGroups.set(group.id, ref); } + this.currentUserGroupsInit = true; }; API.applyGroupMember = function (json) { @@ -29037,28 +29256,10 @@ speechSynthesis.getVoices(); } API.getAllGroupPosts({ groupId - }).then((args2) => { - if (groupId === args2.params.groupId) { - for (var post of args2.posts) { - post.title = this.replaceBioSymbols(post.title); - post.text = this.replaceBioSymbols(post.text); - } - if (args2.posts.length > 0) { - D.announcement = args2.posts[0]; - } - D.posts = args2.posts; - this.updateGroupPostSearch(); - } }); if (D.inGroup) { API.getGroupInstances({ groupId - }).then((args3) => { - if (groupId === args3.params.groupId) { - this.applyGroupDialogInstances( - args3.json.instances - ); - } }); } if (this.$refs.groupDialogTabs.currentName === '0') { @@ -29108,6 +29309,9 @@ speechSynthesis.getVoices(); case 'Moderation Tools': this.showGroupMemberModerationDialog(D.id); break; + case 'Create Post': + this.showGroupPostEditDialog(D.id, null); + break; case 'Leave Group': this.leaveGroup(D.id); break; @@ -29272,9 +29476,9 @@ speechSynthesis.getVoices(); // ignore this event if we were the one to trigger it return; } - if (this.groupDialog.visible && this.groupDialog.id === groupId) { - this.showGroupDialog(groupId); - } + // if (this.groupDialog.visible && this.groupDialog.id === groupId) { + // this.showGroupDialog(groupId); + // } if (!API.currentUserGroups.has(groupId)) { API.currentUserGroups.set(groupId, { id: groupId, @@ -30839,6 +31043,96 @@ speechSynthesis.getVoices(); } }; + $app.data.groupPostEditDialog = { + visible: false, + groupRef: {}, + title: '', + text: '', + sendNotification: true, + visibility: 'group', + roleIds: [], + postId: '', + groupId: '' + }; + + $app.methods.showGroupPostEditDialog = function (groupId, post) { + this.$nextTick(() => adjustDialogZ(this.$refs.groupPostEditDialog.$el)); + var D = this.groupPostEditDialog; + D.sendNotification = true; + D.groupRef = {}; + D.title = ''; + D.text = ''; + D.visibility = 'group'; + D.roleIds = []; + D.postId = ''; + D.groupId = groupId; + $app.gallerySelectDialog.selectedFileId = ''; + $app.gallerySelectDialog.selectedImageUrl = ''; + if (post) { + D.title = post.title; + D.text = post.text; + D.visibility = post.visibility; + D.roleIds = post.roleIds; + D.postId = post.id; + $app.gallerySelectDialog.selectedFileId = post.imageId; + $app.gallerySelectDialog.selectedImageUrl = post.imageUrl; + } + API.getCachedGroup({ groupId }).then((args) => { + D.groupRef = args.ref; + }); + D.visible = true; + }; + + $app.methods.editGroupPost = function () { + var D = this.groupPostEditDialog; + if (!D.groupId || !D.postId) { + return; + } + var params = { + groupId: D.groupId, + postId: D.postId, + title: D.title, + text: D.text, + roleIds: D.roleIds, + visibility: D.visibility, + imageId: null + }; + if (this.gallerySelectDialog.selectedFileId) { + params.imageId = this.gallerySelectDialog.selectedFileId; + } + API.editGroupPost(params).then((args) => { + this.$message({ + message: 'Group post edited', + type: 'success' + }); + return args; + }); + D.visible = false; + }; + + $app.methods.createGroupPost = function () { + var D = this.groupPostEditDialog; + var params = { + groupId: D.groupId, + title: D.title, + text: D.text, + roleIds: D.roleIds, + visibility: D.visibility, + imageId: null + }; + if (this.gallerySelectDialog.selectedFileId) { + params.imageId = this.gallerySelectDialog.selectedFileId; + } + API.createGroupPost(params).then((args) => { + this.$message({ + message: 'Group post created', + type: 'success' + }); + return args; + }); + D.visible = false; + }; + // #endregion // #region | V-Bucks diff --git a/html/src/index.pug b/html/src/index.pug index ba488cf9..0554efc3 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1005,6 +1005,8 @@ html el-dropdown-item(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" icon="el-icon-close" command="Unsubscribe To Announcements" divided) {{ $t('dialog.group.actions.unsubscribe') }} el-dropdown-item(v-else icon="el-icon-check" command="Subscribe To Announcements" divided) {{ $t('dialog.group.actions.subscribe') }} el-dropdown-item(v-if="hasGroupPermission(groupDialog.ref, 'group-invites-manage')" icon="el-icon-message" command="Invite To Group") {{ $t('dialog.group.actions.invite_to_group') }} + template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") + el-dropdown-item(icon="el-icon-tickets" command="Create Post") {{ $t('dialog.group.actions.create_post') }} template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") el-dropdown-item(icon="el-icon-s-operation" command="Moderation Tools") {{ $t('dialog.group.actions.moderation_tools') }} template(v-if="groupDialog.ref.myMember && groupDialog.ref.privacy === 'default'") @@ -1062,8 +1064,16 @@ html span(v-if="groupDialog.announcement.editorId" style="margin-right:5px") ({{ $t('dialog.group.posts.edited_by') }} #[display-name(:userid="groupDialog.announcement.editorId")]) el-tooltip(placement="bottom") template(#content) - span {{ groupDialog.announcement.updatedAt | formatDate('long') }} + span {{ $t('dialog.group.posts.created_at') }} {{ groupDialog.announcement.createdAt | formatDate('long') }} + template(v-if="groupDialog.announcement.updatedAt !== groupDialog.announcement.createdAt") + br + span {{ $t('dialog.group.posts.edited_at') }} {{ groupDialog.announcement.updatedAt | formatDate('long') }} timer(:epoch="Date.parse(groupDialog.announcement.updatedAt)") + template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") + el-tooltip(placement="top" :content="$t('dialog.group.posts.edit_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-edit" size="mini" style="margin-left:5px" @click="showGroupPostEditDialog(groupDialog.id, groupDialog.announcement)") + el-tooltip(placement="top" :content="$t('dialog.group.posts.delete_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="confirmDeleteGroupPost(groupDialog.announcement)") .x-friend-item(style="width:100%;cursor:default") .detail span.name {{ $t('dialog.group.info.rules') }} @@ -1151,8 +1161,16 @@ html span(v-if="post.editorId" style="margin-right:5px") ({{ $t('dialog.group.posts.edited_by') }} #[display-name(:userid="post.editorId")]) el-tooltip(placement="bottom") template(#content) - span {{ post.updatedAt | formatDate('long') }} + span {{ $t('dialog.group.posts.created_at') }} {{ post.createdAt | formatDate('long') }} + template(v-if="post.updatedAt !== post.createdAt") + br + span {{ $t('dialog.group.posts.edited_at') }} {{ post.updatedAt | formatDate('long') }} timer(:epoch="Date.parse(post.updatedAt)") + template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") + el-tooltip(placement="top" :content="$t('dialog.group.posts.edit_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-edit" size="mini" style="margin-left:5px" @click="showGroupPostEditDialog(groupDialog.id, post)") + el-tooltip(placement="top" :content="$t('dialog.group.posts.delete_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="confirmDeleteGroupPost(post)") el-tab-pane(:label="$t('dialog.group.members.header')") template(v-if="groupDialog.visible") span(v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')" style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.all_members') }} @@ -1828,13 +1846,13 @@ html el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name Display Name + span.toggle-name Display Name Change el-radio-group(v-model="sharedFeedFilters.noty.DisplayName" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} .toggle-item - span.toggle-name Trust Level + span.toggle-name Trust Level Change el-radio-group(v-model="sharedFeedFilters.noty.TrustLevel" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} @@ -1850,7 +1868,7 @@ html el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name Group Join/Leave + span.toggle-name Group Join el-radio-group(v-model="sharedFeedFilters.noty['group.informative']" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} @@ -1864,6 +1882,11 @@ html el-radio-group(v-model="sharedFeedFilters.noty['group.joinRequest']" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Transfer Request + el-radio-group(v-model="sharedFeedFilters.noty['group.transfer']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item span.toggle-name Instance Queue Ready el-radio-group(v-model="sharedFeedFilters.noty['group.queueReady']" size="mini" @change="saveSharedFeedFilters") @@ -1874,13 +1897,6 @@ html el-radio-group(v-model="sharedFeedFilters.noty['instance.closed']" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Portal Spawn - el-radio-group(v-model="sharedFeedFilters.noty.PortalSpawn" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} .toggle-item span.toggle-name Video Play el-tooltip(placement="top" style="margin-left:5px" content="Requires VRCX YouTube API option enabled") @@ -1889,12 +1905,12 @@ html el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name Events + span.toggle-name Miscellaneous Events el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name External + span.toggle-name External App el-radio-group(v-model="sharedFeedFilters.noty.External" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} @@ -1937,6 +1953,13 @@ html br .toggle-item span.toggle-name Photon Event Logging + .toggle-item + span.toggle-name Portal Spawn + el-radio-group(v-model="sharedFeedFilters.noty.PortalSpawn" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} .toggle-item span.toggle-name Lobby ChatBox Message el-radio-group(v-model="sharedFeedFilters.noty.ChatBoxMessage" size="mini" @change="saveSharedFeedFilters") @@ -2060,13 +2083,13 @@ html el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name Display Name + span.toggle-name Display Name Change el-radio-group(v-model="sharedFeedFilters.wrist.DisplayName" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} .toggle-item - span.toggle-name Trust Level + span.toggle-name Trust Level Change el-radio-group(v-model="sharedFeedFilters.wrist.TrustLevel" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} @@ -2082,7 +2105,7 @@ html el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name Group Join/Leave + span.toggle-name Group Join el-radio-group(v-model="sharedFeedFilters.wrist['group.informative']" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} @@ -2096,6 +2119,11 @@ html el-radio-group(v-model="sharedFeedFilters.wrist['group.joinRequest']" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Transfer Request + el-radio-group(v-model="sharedFeedFilters.wrist['group.transfer']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item span.toggle-name Instance Queue Ready el-radio-group(v-model="sharedFeedFilters.wrist['group.queueReady']" size="mini" @change="saveSharedFeedFilters") @@ -2106,13 +2134,6 @@ html el-radio-group(v-model="sharedFeedFilters.wrist['instance.closed']" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Portal Spawn - el-radio-group(v-model="sharedFeedFilters.wrist.PortalSpawn" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} .toggle-item span.toggle-name Video Play el-tooltip(placement="top" style="margin-left:5px" content="Requires VRCX YouTube API option enabled") @@ -2121,12 +2142,12 @@ html el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name Events + span.toggle-name Miscellaneous Events el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} .toggle-item - span.toggle-name External + span.toggle-name External App el-radio-group(v-model="sharedFeedFilters.wrist.External" size="mini" @change="saveSharedFeedFilters") el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} @@ -2169,6 +2190,13 @@ html br .toggle-item span.toggle-name Photon Event Logging + .toggle-item + span.toggle-name Portal Spawn + el-radio-group(v-model="sharedFeedFilters.wrist.PortalSpawn" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} .toggle-item span.toggle-name Lobby ChatBox Message el-radio-group(v-model="sharedFeedFilters.wrist.ChatBoxMessage" size="mini" @change="saveSharedFeedFilters") @@ -2417,6 +2445,8 @@ html span {{ $t('dialog.gallery_icons.emoji_animation_frame_count') }} {{ emojiAnimFrameCount }} #[i.el-icon-arrow-down.el-icon--right] el-dropdown-menu(#default="dropdown") el-dropdown-item(v-for="(item) in emojiFrameCountOptions" v-text="item" @click.native="emojiAnimFrameCount = item") + el-checkbox(v-model="emojiAnimLoopPingPong" style="margin-left:10px;margin-right:10px") + span {{ $t('dialog.gallery_icons.emoji_loop_pingpong') }} br span {{ $t('dialog.gallery_icons.flipbook_info') }} br @@ -2424,9 +2454,11 @@ html .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="cursor:default") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") div(style="display:inline-block;margin:5px") + span(v-if="image.loopStyle === 'pingpong'") #[i.el-icon-refresh.el-icon--left] span(style="margin-right:5px") {{ image.animationStyle }} span(v-if="image.framesOverTime" style="margin-right:5px") {{ image.framesOverTime }}fps span(v-if="image.frames" style="margin-right:5px") {{ image.frames }}frames + br div(style="float:right;margin-top:5px") el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) el-button(type="default" @click="deleteEmoji(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") @@ -2981,6 +3013,42 @@ html span(v-if="groupMemberModeration.progressCurrent" style="margin-top:10px") #[i.el-icon-loading(style="margin-left:5px;margin-right:5px")] {{ $t('dialog.group_member_moderation.progress') }} {{ groupMemberModeration.progressCurrent }}/{{ groupMemberModeration.progressTotal }} el-button(v-if="groupMemberModeration.progressCurrent" @click="groupMemberModeration.progressTotal = 0" style="margin-left:5px") {{ $t('dialog.group_member_moderation.cancel') }} + //- dialog: group posts + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupPostEditDialog" :visible.sync="groupPostEditDialog.visible" :title="$t('dialog.group_post_edit.header')" width="650px") + div(v-if="groupPostEditDialog.visible") + h3(v-text="groupPostEditDialog.groupRef.name") + el-form(:model="groupPostEditDialog" label-width="150px") + el-form-item(:label="$t('dialog.group_post_edit.title')") + el-input(v-model="groupPostEditDialog.title" size="mini") + el-form-item(:label="$t('dialog.group_post_edit.message')") + el-input(v-model="groupPostEditDialog.text" type="textarea" :rows="4" :autosize="{ minRows: 4, maxRows: 20 }" style="margin-top:10px" resize="none") + el-form-item + el-checkbox(v-if="!groupPostEditDialog.postId" v-model="groupPostEditDialog.sendNotification" size="small") {{ $t('dialog.group_post_edit.send_notification') }} + el-form-item(:label="$t('dialog.group_post_edit.post_visibility')") + el-radio-group(v-model="groupPostEditDialog.visibility" size="small") + el-radio(label="public") {{ $t('dialog.group_post_edit.visibility_public') }} + el-radio(label="group") {{ $t('dialog.group_post_edit.visibility_group') }} + el-form-item(v-if="groupPostEditDialog.visibility === 'group'" :label="$t('dialog.new_instance.roles')") + el-select(v-model="groupPostEditDialog.roleIds" multiple clearable :placeholder="$t('dialog.new_instance.role_placeholder')" style="width:100%") + el-option-group(:label="$t('dialog.new_instance.role_placeholder')") + el-option.x-friend-item(v-for="role in groupPostEditDialog.groupRef?.roles" :key="role.id" :label="role.name" :value="role.id" style="height:auto;width:478px") + .detail + span.name(v-text="role.name") + el-form-item(:label="$t('dialog.group_post_edit.image')") + template(v-if="gallerySelectDialog.selectedFileId") + div(style="display:inline-block;flex:none;margin-right:5px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="gallerySelectDialog.selectedImageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + img.x-link(v-lazy="gallerySelectDialog.selectedImageUrl" style="height:500px" @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)") + el-button(size="mini" @click="clearImageGallerySelect" style="vertical-align:top") {{ $t('dialog.invite_message.clear_selected_image') }} + template(v-else) + el-button(size="mini" @click="showGallerySelectDialog" style="margin-right:5px") {{ $t('dialog.invite_message.select_image') }} + + template(#footer) + el-button(size="small" @click="groupPostEditDialog.visible = false") {{ $t('dialog.group_post_edit.cancel') }} + el-button(v-if="groupPostEditDialog.postId" size="small" @click="editGroupPost") {{ $t('dialog.group_post_edit.edit_post') }} + el-button(v-else size="small" @click="createGroupPost") {{ $t('dialog.group_post_edit.create_post') }} + //- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="templateDialog" :visible.sync="templateDialog.visible" :title="$t('dialog.template_dialog.header')" width="450px") diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json index e666eeac..c809af33 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -826,6 +826,7 @@ "visibility_everyone": "Visibility Everyone", "visibility_friends": "Visibility Friends", "visibility_hidden": "Visibility Hidden", + "create_post": "Create Post", "moderation_tools": "Moderation Tools", "leave": "Leave Group" }, @@ -853,6 +854,10 @@ "posts": { "header": "Posts", "visibility": "Visibility:", + "edited_at": "Edited At:", + "created_at": "Created At:", + "edit_tooltip": "Edit Post", + "delete_tooltip": "Delete Post", "edited_by": "Edited by:", "search_placeholder": "Search", "posts_count": "Posts: " @@ -1216,6 +1221,7 @@ "emoji_animation_type": "Animated Emoji", "emoji_animation_fps": "FPS:", "emoji_animation_frame_count": "Frame Count:", + "emoji_loop_pingpong": "Loop PingPong", "flipbook_info": "Select a 1024x1024 PNG spritesheet to use as an animated emoji (max FPS 64)" }, "change_content_image": { @@ -1303,6 +1309,19 @@ "selected_roles": "Selected Roles", "remove_roles": "Remove Roles", "add_roles": "Add Roles" + }, + "group_post_edit": { + "header": "Create/Edit Post", + "title": "Title", + "message": "Message", + "send_notification": "Send Notification", + "post_visibility": "Post Visibility", + "visibility_public": "Public", + "visibility_group": "Group", + "image": "Image", + "cancel": "Cancel", + "create_post": "Create Post", + "edit_post": "Edit Post" } }, "prompt": { diff --git a/html/src/mixins/tabs/notifications.pug b/html/src/mixins/tabs/notifications.pug index 5d2c5bf0..f3e60647 100644 --- a/html/src/mixins/tabs/notifications.pug +++ b/html/src/mixins/tabs/notifications.pug @@ -4,7 +4,7 @@ mixin notificationsTab() template(#tool) div(style="margin:0 0 10px;display:flex;align-items:center") el-select(v-model="notificationTable.filters[0].value" @change="saveTableFilters" multiple clearable collapse-tags style="flex:1" :placeholder="$t('view.notification.filter_placeholder')") - el-option(v-once v-for="type in ['requestInvite', 'invite', 'requestInviteResponse', 'inviteResponse', 'friendRequest', 'hiddenFriendRequest', 'message', 'groupChange', 'group.announcement', 'group.informative', 'group.invite', 'group.joinRequest', 'group.queueReady', 'moderation.warning.group', 'instance.closed']" :key="type" :label="type" :value="type") + el-option(v-once v-for="type in ['requestInvite', 'invite', 'requestInviteResponse', 'inviteResponse', 'friendRequest', 'hiddenFriendRequest', 'message', 'groupChange', 'group.announcement', 'group.informative', 'group.invite', 'group.joinRequest', 'group.transfer', 'group.queueReady', 'moderation.warning.group', 'instance.closed']" :key="type" :label="type" :value="type") el-input(v-model="notificationTable.filters[1].value" :placeholder="$t('view.notification.search_placeholder')" style="flex:none;width:150px;margin:0 10px") el-tooltip(placement="bottom" :content="$t('view.notification.refresh_tooltip')" :disabled="hideTooltips") el-button(type="default" :loading="API.isNotificationsLoading" @click="API.refreshNotifications()" icon="el-icon-refresh" circle style="flex:none") @@ -72,28 +72,14 @@ mixin notificationsTab() el-button(type="text" icon="el-icon-check" size="mini" @click="acceptRequestInvite(scope.row)") el-tooltip(placement="top" content="Decline with message" :disabled="hideTooltips") el-button(type="text" icon="el-icon-chat-line-square" size="mini" style="margin-left:5px" @click="showSendInviteRequestResponseDialog(scope.row)") - template(v-else-if="scope.row.type === 'group.invite'") - el-tooltip(placement="top" content="Accept" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-check" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'accept')") - el-tooltip(placement="top" content="Decline" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'decline')") - el-tooltip(placement="top" content="Block invites from group" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-circle-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'block')") - template(v-else-if="scope.row.type === 'group.joinRequest'") - el-tooltip(placement="top" content="Accept" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-check" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'accept')") - el-tooltip(placement="top" content="Decline" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'reject')") - el-tooltip(placement="top" content="Block user from requesting" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-circle-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'block')") - template(v-else-if="scope.row.type === 'group.announcement'") - el-tooltip(placement="top" content="Dismiss" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-check" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'delete')") - el-tooltip(placement="top" content="Unsubscribe" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'unsubscribe')") - template(v-else-if="scope.row.type === 'group.informative'") - el-tooltip(placement="top" content="Dismiss" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-check" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, 'delete')") + template(v-if="scope.row.responses") + template(v-for="response in scope.row.responses") + el-tooltip(placement="top" :content="response.text" :disabled="hideTooltips") + el-button(v-if="response.icon === 'check'" type="text" icon="el-icon-check" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, response.type)") + el-button(v-else-if="response.icon === 'cancel'" type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, response.type)") + el-button(v-else-if="response.icon === 'ban'" type="text" icon="el-icon-circle-close" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, response.type)") + el-button(v-else-if="response.icon === 'bell-slash'" type="text" icon="el-icon-bell" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, response.type)") + el-button(v-else type="text" icon="el-icon-collection-tag" size="mini" style="margin-left:5px" @click="sendNotificationResponse(scope.row.id, scope.row.responses, response.type)") template(v-if="scope.row.type !== 'requestInviteResponse' && scope.row.type !== 'inviteResponse' && scope.row.type !== 'message' && scope.row.type !== 'groupChange' && !scope.row.type.includes('group.') && !scope.row.type.includes('moderation.') && !scope.row.type.includes('instance.')") el-tooltip(placement="top" content="Decline" :disabled="hideTooltips") el-button(type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="hideNotification(scope.row)") diff --git a/html/src/repository/database.js b/html/src/repository/database.js index fc471e35..9956b2bf 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -2480,6 +2480,12 @@ class Database { ); } + async fixBrokenGroupChange() { + await sqliteService.executeNonQuery( + `DELETE FROM ${Database.userPrefix}_notifications WHERE type = 'groupChange'` + ); + } + async updateTableForGroupNames() { var tables = []; await sqliteService.execute((dbRow) => { diff --git a/html/src/vr.js b/html/src/vr.js index 01eef3a7..c692f37f 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -597,6 +597,9 @@ Vue.component('marquee-text', MarqueeText); case 'group.joinRequest': text = noty.message; break; + case 'group.transfer': + text = noty.message; + break; case 'group.queueReady': text = noty.message; break; diff --git a/html/src/vr.pug b/html/src/vr.pug index 2441777e..89f74f48 100644 --- a/html/src/vr.pug +++ b/html/src/vr.pug @@ -144,6 +144,11 @@ html span.extra span.time {{ feed.created_at | formatDate }} | 🏷️ #[span.name(v-text="feed.message")] + div(v-else-if="feed.type === 'group.transfer'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate }} + | 🏷️ #[span.name(v-text="feed.message")] div(v-else-if="feed.type === 'group.queueReady'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -364,6 +369,11 @@ html span.extra span.time {{ feed.created_at | formatDate }} | #[span.name(v-text="feed.message")] + div(v-else-if="feed.type === 'group.transfer'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate }} + | #[span.name(v-text="feed.message")] div(v-else-if="feed.type === 'group.queueReady'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra