diff --git a/html/src/app.js b/html/src/app.js index 4022e9ea..1139380d 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -409,11 +409,15 @@ speechSynthesis.getVoices(); this.$throw(0, 'Invalid JSON response'); } if ( - response.status === 504 || - response.status === 502 || - (response.status === 429 && - init.url.endswith('/instances/groups ')) + response.status === 429 && + init.url.endsWith('/instances/groups') ) { + $app.nextGroupInstanceRefresh = 120; // 1min + throw new Error( + `${response.status}: rate limited ${endpoint}` + ); + } + if (response.status === 504 || response.status === 502) { // ignore expected API errors throw new Error( `${response.status}: ${response.data} ${endpoint}` @@ -445,7 +449,7 @@ speechSynthesis.getVoices(); status === 401 && data.error.message === '"Missing Credentials"' ) { - if (endpoint.substring(0, 10) === 'auth/user?') { + if (endpoint.substring(0, 9) === 'auth/user') { this.$emit('AUTOLOGIN'); } throw new Error('401: Missing Credentials'); @@ -848,7 +852,7 @@ speechSynthesis.getVoices(); "" + '' + '{{ text }}' + - '({{ groupName }})' + + '({{ groupName }})' + '' + '', props: { @@ -952,7 +956,7 @@ speechSynthesis.getVoices(); } }, showGroupDialog() { - if (!this.location) { + if (!this.location || !this.link) { return; } var L = API.parseLocation(this.location); @@ -3739,6 +3743,7 @@ speechSynthesis.getVoices(); this.cachedFavoriteGroups.clear(); this.cachedFavoriteGroupsByTypeName.clear(); this.currentUserGroups.clear(); + this.queuedInstances.clear(); this.favoriteFriendGroups = []; this.favoriteWorldGroups = []; this.favoriteAvatarGroups = []; @@ -4716,6 +4721,7 @@ speechSynthesis.getVoices(); case 'group-role-updated': var groupId = content.role.groupId; console.log('group-role-updated', content); + // content { // role: { // createdAt: string, @@ -4733,7 +4739,9 @@ speechSynthesis.getVoices(); case 'group-member-updated': var groupId = content.groupId; + $app.onGroupJoined(groupId); console.log('group-member-updated', content); + // content { // groupId: string, // id: string, @@ -4747,6 +4755,81 @@ speechSynthesis.getVoices(); // } break; + case 'instance-queue-joined': + case 'instance-queue-position': + var instanceId = content.instanceLocation; + var ref = this.queuedInstances.get(instanceId); + if (typeof ref === 'undefined') { + ref = { + $msgBox: null, + $groupName: '', + $worldName: '', + location: instanceId, + position: 0, + queueSize: 0 + }; + } + if (content.position) { + ref.position = content.position; + } + if (content.queueSize) { + ref.queueSize = content.queueSize; + } + if (!ref.$msgBox) { + ref.$msgBox = $app.$message({ + message: '', + type: 'info', + duration: 0, + showClose: true, + customClass: 'vrc-instance-queue-message' + }); + } + if (!ref.$groupName) { + $app.getGroupName(instanceId).then((name) => { + ref.$groupName = name; + ref.$msgBox.message = `You are in position ${ref.position} of ${ref.queueSize} in the queue for ${name} `; + }); + } + if (!ref.$worldName) { + $app.getWorldName(instanceId).then((name) => { + ref.$worldName = name; + }); + } + + ref.$msgBox.message = `You are in position ${ref.position} of ${ref.queueSize} in the queue for ${ref.$groupName} `; + API.queuedInstances.set(instanceId, ref); + break; + + case 'instance-queue-ready': + var instanceId = content.instanceLocation; + // var expiry = Date.parse(content.expiry); + var ref = this.queuedInstances.get(instanceId); + if (typeof ref !== 'undefined') { + ref.$msgBox.close(); + this.queuedInstances.delete(instanceId); + } + var L = this.parseLocation(instanceId); + var group = this.cachedGroups.get(L.groupId); + var groupName = group?.name ? group.name : ''; + var worldName = ref.$worldName ? ref.$worldName : ''; + $app.$message({ + message: `Instance ready to join ${groupName} - ${worldName}`, + type: 'success' + }); + var noty = { + created_at: new Date().toJSON(), + type: 'group.queueReady', + imageUrl: group?.iconUrl, + message: `Instance ready to join ${groupName}- ${worldName}`, + location: instanceId, + groupName, + worldName + }; + $app.queueNotificationNoty(noty); + $app.notificationTable.data.push(noty); + $app.updateSharedFeed(true); + break; + default: console.log('Unknown pipeline type', args.json); } @@ -6198,6 +6281,9 @@ speechSynthesis.getVoices(); case 'group.joinRequest': this.speak(noty.message); break; + case 'group.queueReady': + this.speak(noty.message); + break; case 'PortalSpawn': if (noty.displayName) { this.speak( @@ -6414,6 +6500,9 @@ speechSynthesis.getVoices(); case 'group.joinRequest': AppApi.XSNotification('VRCX', noty.message, timeout, image); break; + case 'group.queueReady': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; case 'PortalSpawn': if (noty.displayName) { AppApi.XSNotification( @@ -6680,6 +6769,13 @@ speechSynthesis.getVoices(); image ); break; + case 'group.queueReady': + AppApi.DesktopNotification( + 'Instance Queue Ready', + noty.message, + image + ); + break; case 'PortalSpawn': if (noty.displayName) { AppApi.DesktopNotification( @@ -12137,6 +12233,7 @@ speechSynthesis.getVoices(); this.searchAvatarResults = []; this.searchAvatarPage = []; this.searchAvatarPageNum = 0; + this.searchGroupParams = {}; this.searchGroupResults = []; }; @@ -13969,6 +14066,7 @@ speechSynthesis.getVoices(); 'group.informative': 'On', 'group.invite': 'On', 'group.joinRequest': 'Off', + 'group.queueReady': 'On', PortalSpawn: 'Everyone', Event: 'On', VideoPlay: 'Off', @@ -14005,6 +14103,7 @@ speechSynthesis.getVoices(); 'group.informative': 'On', 'group.invite': 'On', 'group.joinRequest': 'On', + 'group.queueReady': 'On', PortalSpawn: 'Everyone', Event: 'On', VideoPlay: 'On', @@ -14046,6 +14145,10 @@ speechSynthesis.getVoices(); $app.data.sharedFeedFilters.wrist['group.invite'] = 'On'; $app.data.sharedFeedFilters.wrist['group.joinRequest'] = 'On'; } + if (!$app.data.sharedFeedFilters.noty['group.queueReady']) { + $app.data.sharedFeedFilters.noty['group.queueReady'] = 'On'; + $app.data.sharedFeedFilters.wrist['group.queueReady'] = 'On'; + } $app.data.trustColor = JSON.parse( configRepository.getString( @@ -14574,7 +14677,11 @@ speechSynthesis.getVoices(); if (instanceId) { var shortName = urlParams.get('shortName'); var location = `${worldId}:${instanceId}`; - return this.verifyShortName(location, shortName); + if (shortName) { + return this.verifyShortName(location, shortName); + } + this.showWorldDialog(location); + return true; } else if (worldId) { this.showWorldDialog(worldId); return true; @@ -16752,17 +16859,19 @@ speechSynthesis.getVoices(); return; } var instances = {}; - for (var [id, occupants] of D.ref.instances) { - instances[id] = { - id, - tag: `${D.id}:${id}`, - occupants, - friendCount: 0, - full: false, - users: [], - shortName: '', - json: {} - }; + if (D.ref.instances) { + for (var [id, occupants] of D.ref.instances) { + instances[id] = { + id, + tag: `${D.id}:${id}`, + occupants, + friendCount: 0, + full: false, + users: [], + shortName: '', + json: {} + }; + } } var { instanceId, shortName } = D.$location; if (instanceId && typeof instances[instanceId] === 'undefined') { @@ -16934,12 +17043,12 @@ speechSynthesis.getVoices(); id: instance.instanceId, tag: instance.location, $location: {}, - occupants: instance.memberCount, + occupants: instance.userCount, friendCount: 0, full: false, users: [], - shortName: '', - json: {} + shortName: instance.shortName, + json: instance }; } } @@ -21795,7 +21904,6 @@ speechSynthesis.getVoices(); $app.methods.getCurrentUserGroups = async function () { var args = await API.getGroups({ n: 100, userId: API.currentUser.id }); - this.inviteGroupDialog.groups = args.json; API.currentUserGroups.clear(); args.json.forEach((group) => { API.currentUserGroups.set(group.id, group); @@ -24643,6 +24751,7 @@ speechSynthesis.getVoices(); API.cachedGroups = new Map(); API.currentUserGroups = new Map(); + API.queuedInstances = new Map(); /* params: { @@ -25069,9 +25178,12 @@ speechSynthesis.getVoices(); */ API.getGroupInstances = function (params) { - return this.call(`groups/${params.groupId}/instances`, { - method: 'GET' - }).then((json) => { + return this.call( + `users/${this.currentUser.id}/instances/groups/${params.groupId}`, + { + method: 'GET' + } + ).then((json) => { var args = { json, params @@ -25082,7 +25194,13 @@ speechSynthesis.getVoices(); }; API.$on('GROUP:INSTANCES', function (args) { - for (var json of args.json) { + for (var json of args.json.instances) { + this.$emit('INSTANCE', { + json, + params: { + fetchedAt: args.json.fetchedAt + } + }); this.$emit('WORLD', { json: json.world, params: { @@ -25146,6 +25264,13 @@ speechSynthesis.getVoices(); fetchedAt: args.json.fetchedAt } }); + this.$emit('WORLD', { + json: json.world, + params: { + worldId: json.world.id + } + }); + json.world = this.applyWorld(json.world); var ref = this.cachedGroups.get(json.ownerId); if (typeof ref === 'undefined') { @@ -25482,7 +25607,9 @@ speechSynthesis.getVoices(); groupId }).then((args3) => { if (groupId === args3.params.groupId) { - this.applyGroupDialogInstances(args3.json); + this.applyGroupDialogInstances( + args3.json.instances + ); } }); } @@ -25900,8 +26027,7 @@ speechSynthesis.getVoices(); groupName: '', userId: '', userIds: '', - userObject: {}, - groups: [] + userObject: {} }; $app.methods.showInviteGroupDialog = function (groupId, userId) { @@ -25914,7 +26040,6 @@ speechSynthesis.getVoices(); D.userId = userId; D.userObject = {}; D.visible = true; - D.loading = true; if (groupId) { API.getCachedGroup({ groupId diff --git a/html/src/app.scss b/html/src/app.scss index 67a10e65..b18e0981 100644 --- a/html/src/app.scss +++ b/html/src/app.scss @@ -700,3 +700,8 @@ i.x-user-status.busy { .changelog-dialog img { width: 100%; } + +.vrc-instance-queue-message { + padding: 3px; + top: 0 !important; +} diff --git a/html/src/index.pug b/html/src/index.pug index c77bbf89..bd942623 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1552,6 +1552,11 @@ html el-radio-group(v-model="sharedFeedFilters.noty['group.joinRequest']" size="mini") 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") + 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") @@ -1764,6 +1769,11 @@ html el-radio-group(v-model="sharedFeedFilters.wrist['group.joinRequest']" size="mini") 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") + 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") @@ -2347,8 +2357,8 @@ html span {{ $t('dialog.invite_to_group.description') }} br el-select(v-model="inviteGroupDialog.groupId" clearable :placeholder="$t('dialog.invite_to_group.choose_group_placeholder')" filterable :disabled="inviteGroupDialog.loading" @change="isAllowedToInviteToGroup" style="margin-top:15px") - el-option-group(v-if="inviteGroupDialog.groups.length" :label="$t('dialog.invite_to_group.groups')" style="width:410px") - el-option.x-friend-item(v-for="group in inviteGroupDialog.groups" :key="group.id" :label="group.name" :value="group.id" style="height:auto") + el-option-group(v-if="API.currentUserGroups.size" :label="$t('dialog.invite_to_group.groups')" style="width:410px") + el-option.x-friend-item(v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto") .avatar img(v-lazy="group.iconUrl") .detail diff --git a/html/src/mixins/tabs/notifications.pug b/html/src/mixins/tabs/notifications.pug index 9cd918c9..ec66e54a 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', 'group.announcement', 'group.informative', 'group.invite', 'group.joinRequest', 'moderation.warning.group']" :key="type" :label="type" :value="type") + el-option(v-once v-for="type in ['requestInvite', 'invite', 'requestInviteResponse', 'inviteResponse', 'friendRequest', 'hiddenFriendRequest', 'message', 'group.announcement', 'group.informative', 'group.invite', 'group.joinRequest', 'group.queueReady', 'moderation.warning.group']" :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") @@ -20,6 +20,10 @@ mixin notificationsTab() template(#content) location(v-if="scope.row.details" :location="scope.row.details.worldId" :hint="scope.row.details.worldName" :grouphint="scope.row.details.groupName" :link="false") span.x-link(v-text="scope.row.type" @click="showWorldDialog(scope.row.details.worldId)") + el-tooltip(v-if="scope.row.type === 'group.queueReady'" placement="top") + template(#content) + location(v-if="scope.row.location" :location="scope.row.location" :hint="scope.row.worldName" :grouphint="scope.row.groupName" :link="false") + span.x-link(v-text="scope.row.type" @click="showWorldDialog(scope.row.location)") template(v-else-if="scope.row.link") el-tooltip(placement="top" :content="scope.row.linkText" :disabled="hideTooltips") span.x-link(v-text="scope.row.type" @click="openNotificationLink(scope.row.link)") @@ -84,6 +88,9 @@ mixin notificationsTab() template(v-if="scope.row.type !== 'requestInviteResponse' && scope.row.type !== 'inviteResponse' && scope.row.type !== 'message' && !scope.row.type.includes('group.') && !scope.row.type.includes('moderation.')") 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'") + el-tooltip(placement="top" content="Delete log" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="deleteNotificationLog(scope.row)") template(v-if="scope.row.type !== 'friendRequest' && scope.row.type !== 'hiddenFriendRequest' && !scope.row.type.includes('group.') && !scope.row.type.includes('moderation.')") el-tooltip(placement="top" content="Delete log" :disabled="hideTooltips") el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="deleteNotificationLog(scope.row)") diff --git a/html/src/vr.js b/html/src/vr.js index f6e7f223..5cd50588 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -580,6 +580,9 @@ Vue.component('marquee-text', MarqueeText); case 'group.joinRequest': text = escapeTag(noty.message); break; + case 'group.queueReady': + text = escapeTag(noty.message); + break; case 'PortalSpawn': if (noty.displayName) { text = `${ diff --git a/html/src/vr.pug b/html/src/vr.pug index f8a22a1b..474cb224 100644 --- a/html/src/vr.pug +++ b/html/src/vr.pug @@ -139,6 +139,11 @@ html 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 + span.time {{ feed.created_at | formatDate }} + | 📨 #[span.name(v-text="feed.message")] div(v-else-if="feed.type === 'PortalSpawn'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -339,6 +344,11 @@ html 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 + span.time {{ feed.created_at | formatDate }} + | #[span.name(v-text="feed.message")] div(v-else-if="feed.type === 'PortalSpawn'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra