diff --git a/html/src/app.js b/html/src/app.js index fa827ef0..fe0cfe0c 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -3562,6 +3562,18 @@ speechSynthesis.getVoices(); } else if (json.title) { json.message = json.title; } + if (json.type === 'boop') { + if (!json.imageUrl && json.details?.emojiId?.startsWith('file_')) { + // JANK: create image url from fileId + json.imageUrl = `https://api.vrchat.cloud/api/1/file/${json.details.emojiId}/${json.details.emojiVersion}`; + } + if (!json.details?.emojiId?.startsWith('file_')) { + // JANK: get emoji name from emojiId + json.message = `${json.senderUsername} Booped you! with ${$app.getEmojiName(json.details.emojiId)}`; + } else { + json.message = `${json.senderUsername} Booped you! with custom emoji`; + } + } this.$emit('NOTIFICATION', { json, params: { @@ -3604,14 +3616,20 @@ speechSynthesis.getVoices(); return this.call(`notifications/${params.notificationId}/respond`, { method: 'POST', params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('NOTIFICATION:RESPONSE', args); - return args; - }); + }) + .then((json) => { + var args = { + json, + params + }; + this.$emit('NOTIFICATION:RESPONSE', args); + return args; + }) + .catch((err) => { + // something went wrong, lets assume it's already expired + this.$emit('NOTIFICATION:HIDE', { params }); + throw err; + }); }; API.$on('NOTIFICATION:RESPONSE', function (args) { @@ -6806,6 +6824,9 @@ speechSynthesis.getVoices(); `${noty.previousDisplayName} changed their name to ${noty.displayName}` ); break; + case 'boop': + this.speak(noty.message); + break; case 'groupChange': this.speak(`${noty.senderUsername} ${noty.message}`); break; @@ -7037,6 +7058,9 @@ speechSynthesis.getVoices(); image ); break; + case 'boop': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; case 'groupChange': AppApi.XSNotification( 'VRCX', @@ -7372,6 +7396,16 @@ speechSynthesis.getVoices(); image ); break; + case 'boop': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; case 'groupChange': AppApi.OVRTNotification( playOvrtHudNotifications, @@ -7737,6 +7771,13 @@ speechSynthesis.getVoices(); image ); break; + case 'boop': + AppApi.DesktopNotification( + noty.senderUsername, + noty.message, + image + ); + break; case 'groupChange': AppApi.DesktopNotification( noty.senderUsername, @@ -15827,6 +15868,7 @@ speechSynthesis.getVoices(); Unfriend: 'On', DisplayName: 'VIP', TrustLevel: 'VIP', + boop: 'Off', groupChange: 'On', 'group.announcement': 'On', 'group.informative': 'On', @@ -15868,6 +15910,7 @@ speechSynthesis.getVoices(); Unfriend: 'On', DisplayName: 'Friends', TrustLevel: 'Friends', + boop: 'On', groupChange: 'On', 'group.announcement': 'On', 'group.informative': 'On', @@ -15941,6 +15984,10 @@ speechSynthesis.getVoices(); $app.data.sharedFeedFilters.noty['group.transfer'] = 'On'; $app.data.sharedFeedFilters.wrist['group.transfer'] = 'On'; } + if (!$app.data.sharedFeedFilters.noty.boop) { + $app.data.sharedFeedFilters.noty.boop = 'Off'; + $app.data.sharedFeedFilters.wrist.boop = 'On'; + } $app.data.trustColor = JSON.parse( await configRepository.getString( @@ -18539,6 +18586,8 @@ speechSynthesis.getVoices(); this.showGalleryDialog(); } else if (command === 'Invite To Group') { this.showInviteGroupDialog('', D.id); + } else if (command === 'Send Boop') { + this.showSendBoopDialog(D.id); } else if (command === 'Hide Avatar') { if (D.isHideAvatar) { this.setPlayerModeration(D.id, 0); @@ -20527,11 +20576,8 @@ speechSynthesis.getVoices(); D.shortName = ''; D.secureOrShortName = ''; API.getGroupPermissions({ userId: API.currentUser.id }); - if (D.selectedTab === '0') { - this.buildInstance(); - } else { - this.buildLegacyInstance(); - } + this.buildInstance(); + this.buildLegacyInstance(); this.updateNewInstanceDialog(); D.visible = true; }; @@ -24219,8 +24265,6 @@ speechSynthesis.getVoices(); // YouTube API - $app.data.youTubeApiKey = ''; - $app.data.youTubeApiDialog = { visible: false }; @@ -24875,11 +24919,22 @@ speechSynthesis.getVoices(); $app.galleryTable = []; }); - $app.methods.showGalleryDialog = function () { + $app.methods.showGalleryDialog = function (pageNum) { + this.$nextTick(() => adjustDialogZ(this.$refs.galleryDialog.$el)); this.galleryDialogVisible = true; this.refreshGalleryTable(); this.refreshVRCPlusIconsTable(); this.refreshEmojiTable(); + workerTimers.setTimeout(() => this.setGalleryTab(pageNum), 100); + }; + + $app.methods.setGalleryTab = function (pageNum) { + if ( + typeof pageNum !== 'undefined' && + typeof this.$refs.galleryTabs !== 'undefined' + ) { + this.$refs.galleryTabs.setCurrentName(`${pageNum}`); + } }; $app.methods.refreshGalleryTable = function () { @@ -32588,6 +32643,7 @@ speechSynthesis.getVoices(); }); // #endregion + // #region | Settings: Zoom $app.data.zoomLevel = ((await AppApi.GetZoom()) + 10) * 10; @@ -32599,6 +32655,80 @@ speechSynthesis.getVoices(); AppApi.SetZoom(this.zoomLevel / 10 - 10); }; + // #endregion + // #region | Boops + + API.sendBoop = function (params) { + return this.call(`users/${params.userId}/boop`, { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('BOOP:SEND', args); + return args; + }); + }; + + $app.methods.sendBoop = function () { + var D = this.sendBoopDialog; + this.dismissBoop(D.userId); + API.sendBoop({ userId: D.userId, emojiId: D.fileId }); + D.visible = false; + }; + + $app.methods.dismissBoop = function (userId) { + // JANK: This is a hack to remove boop notifications when responding + var array = this.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + var ref = array[i]; + if ( + ref.type !== 'boop' || + ref.$isExpired || + ref.senderUserId !== userId + ) { + continue; + } + API.sendNotificationResponse({ + notificationId: ref.id, + responseType: 'delete', + responseData: '' + }); + } + }; + + $app.data.sendBoopDialog = { + visible: false, + userId: '', + fileId: '' + }; + + $app.methods.showSendBoopDialog = function (userId) { + this.$nextTick(() => adjustDialogZ(this.$refs.sendBoopDialog.$el)); + var D = this.sendBoopDialog; + D.userId = userId; + D.visible = true; + if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) { + this.refreshEmojiTable(); + } + }; + + $app.methods.getEmojiValue = function (emojiName) { + return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`; + }; + + $app.methods.getEmojiName = function (emojiValue) { + // uppercase first letter of each word + return emojiValue + .replace('vrchat_', '') + .replace(/_/g, ' ') + .replace(/\b\w/g, (l) => l.toUpperCase()); + }; + + // #endregion + $app = new Vue($app); window.$app = $app; })(); diff --git a/html/src/index.pug b/html/src/index.pug index becb4a81..9fd8f441 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -300,6 +300,7 @@ html el-dropdown-item(v-else-if="userDialog.outgoingRequest" icon="el-icon-close" command="Cancel Friend Request") {{ $t('dialog.user.actions.cancel_friend_request') }} el-dropdown-item(v-else icon="el-icon-plus" command="Send Friend Request") {{ $t('dialog.user.actions.send_friend_request') }} el-dropdown-item(icon="el-icon-message" command="Invite To Group") {{ $t('dialog.user.actions.invite_to_group') }} + el-dropdown-item(icon="el-icon-thumb" command="Send Boop") {{ $t('dialog.user.actions.send_boop') }} el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author" divided) {{ $t('dialog.user.actions.show_avatar_author') }} el-dropdown-item(icon="el-icon-s-custom" command="Show Fallback Avatar Details") {{ $t('dialog.user.actions.show_fallback_avatar') }} el-dropdown-item(icon="el-icon-tickets" command="Previous Instances") {{ $t('dialog.user.actions.show_previous_instances') }} @@ -1895,6 +1896,11 @@ html 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 Boop + el-radio-group(v-model="sharedFeedFilters.noty.boop" 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 Change el-tooltip(placement="top" style="margin-left:5px" content="When you've left or been kicked from a group, group name changed, group owner changed, role added/removed") @@ -2144,6 +2150,11 @@ html 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 Boop + el-radio-group(v-model="sharedFeedFilters.wrist.boop" 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 Change el-tooltip(placement="top" style="margin-left:5px" content="When you've left or been kicked from a group, group name changed, group owner changed, role added/removed") @@ -2452,7 +2463,7 @@ html span(style="padding-bottom:10px") {{ $t('dialog.gallery_icons.description') }} br br - el-tabs(type="card") + el-tabs(type="card" ref="galleryTabs") el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogGalleryLoading") span(slot="label") {{ $t('dialog.gallery_icons.gallery') }} span(style="color:#909399;font-size:12px;margin-left:5px") {{ galleryTable.length }}/64 @@ -3267,6 +3278,61 @@ html 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') }} + //- dialog: send boop + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendBoopDialog" :visible.sync="sendBoopDialog.visible" :title="$t('dialog.boop_dialog.header')" width="450px") + div(v-if="sendBoopDialog.visible") + el-button(size="small" @click="showGalleryDialog(2)") {{ $t('dialog.boop_dialog.emoji_manager') }} + br + br + el-select(v-model="sendBoopDialog.userId" :placeholder="$t('dialog.new_instance.instance_creator_placeholder')" filterable style="width:100%") + el-option-group(v-if="friendsGroup0.length" :label="$t('side_panel.favorite')") + el-option.x-friend-item(v-for="friend in friendsGroup0" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="friendsGroup1.length" :label="$t('side_panel.online')") + el-option.x-friend-item(v-for="friend in friendsGroup1" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="friendsGroup2.length" :label="$t('side_panel.active')") + el-option.x-friend-item(v-for="friend in friendsGroup2" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="friendsGroup3.length" :label="$t('side_panel.offline')") + el-option.x-friend-item(v-for="friend in friendsGroup3" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + br + br + el-select(v-model="sendBoopDialog.fileId" clearable :placeholder="$t('dialog.boop_dialog.select_emoji')" size="small" style="width:100%" popper-class="max-height-el-select") + el-option-group(:label="$t('dialog.boop_dialog.my_emojis')") + el-option(v-if="image.versions && image.versions.length > 0" v-for="image in emojiTable" :key="image.id" :value="image.id" style="width:100%;height:100%") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden;width:200px;height:200px;padding:10px") + template(v-if="image.frames") + .avatar(:style="generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.animationStyle)") + template(v-else) + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url" style="width:200px;height:200px") + el-option-group(:label="$t('dialog.boop_dialog.default_emojis')") + el-option(v-for="emojiName in photonEmojis" :key="emojiName" :value="getEmojiValue(emojiName)" style="width:100%;height:100%") + span(v-text="emojiName") + template(#footer) + el-button(size="small" @click="sendBoopDialog.visible = false") {{ $t('dialog.boop_dialog.cancel') }} + el-button(size="small" @click="sendBoop" :disabled="!sendBoopDialog.userId || !sendBoopDialog.fileId") {{ $t('dialog.boop_dialog.send') }} //- 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 559030d8..1a8f4e68 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -560,6 +560,7 @@ "request_invite": "Request Invite", "request_invite_with_message": "Request Invite With Message", "invite_to_group": "Invite To Group", + "send_boop": "Send Boop", "manage_gallery_icon": "Manage Photos/Icons/Emojis", "accept_friend_request": "Accept Friend Request", "decline_friend_request": "Decline Friend Request", @@ -1365,6 +1366,15 @@ "cancel": "Cancel", "create_post": "Create Post", "edit_post": "Edit Post" + }, + "boop_dialog": { + "header": "Boop", + "emoji_manager": "Emoji Manager", + "select_emoji": "Select Emoji", + "my_emojis": "My Emojis", + "default_emojis": "Default Emojis", + "cancel": "Cancel", + "send": "Send" } }, "prompt": { diff --git a/html/src/mixins/tabs/notifications.pug b/html/src/mixins/tabs/notifications.pug index 66b4c5df..d688f4a5 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.transfer', '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', 'boop', '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") @@ -79,8 +79,10 @@ mixin notificationsTab() 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-if="response.icon === 'reply' && scope.row.type === 'boop'" type="text" icon="el-icon-chat-line-square" size="mini" style="margin-left:5px" @click="showSendBoopDialog(scope.row.senderUserId)") + el-button(v-else-if="response.icon === 'reply'" type="text" icon="el-icon-chat-line-square" 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.')") + template(v-if="scope.row.type !== 'requestInviteResponse' && scope.row.type !== 'inviteResponse' && scope.row.type !== 'message' && scope.row.type !== 'boop' && 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)") template(v-if="scope.row.type === 'group.queueReady'") diff --git a/html/src/repository/database.js b/html/src/repository/database.js index f9eefc12..1c1f8345 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -821,6 +821,9 @@ class Database { ...row.details } }; + if (entry.imageUrl && !entry.details.imageUrl) { + entry.details.imageUrl = entry.imageUrl; + } var expired = 0; if (row.$isExpired) { expired = 1; diff --git a/html/src/vr.js b/html/src/vr.js index 8a0804ef..c862b23d 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -599,6 +599,9 @@ Vue.component('marquee-text', MarqueeText); case 'DisplayName': text = `${noty.previousDisplayName} changed their name to ${noty.displayName}`; break; + case 'boop': + text = noty.message; + break; case 'groupChange': text = `${noty.senderUsername} ${noty.message}`; break; diff --git a/html/src/vr.pug b/html/src/vr.pug index 45cb4e90..3f53cdca 100644 --- a/html/src/vr.pug +++ b/html/src/vr.pug @@ -119,6 +119,11 @@ html span.extra span.time {{ feed.created_at | formatDate }} | 🤝 #[span.name(v-text="feed.displayName")] {{ feed.previousTrustLevel }} #[i.el-icon-right] {{ feed.trustLevel }} + div(v-else-if="feed.type === 'boop'" 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 === 'groupChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -344,6 +349,11 @@ html span.extra span.time {{ feed.created_at | formatDate }} | #[span.name(v-text="feed.displayName")] trust level is now {{ feed.trustLevel }} + div(v-else-if="feed.type === 'boop'" 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 === 'groupChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra