diff --git a/Dotnet/PWI/WorldDBManager.cs b/Dotnet/PWI/WorldDBManager.cs index 7bf50eda3..7a7d62887 100644 --- a/Dotnet/PWI/WorldDBManager.cs +++ b/Dotnet/PWI/WorldDBManager.cs @@ -740,7 +740,7 @@ public void Stop() listener?.Stop(); listener?.Close(); } - catch (ObjectDisposedException ex) + catch (ObjectDisposedException) { // ignore } diff --git a/html/src/app.js b/html/src/app.js index 9c60db4a4..4a50af047 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -1081,6 +1081,7 @@ speechSynthesis.getVoices(); '
' + '' + '
' + + '{{ $t("dialog.user.info.close_instance") }}

' + 'PC: {{ platforms.standalonewindows }}
' + 'Android: {{ platforms.android }}
' + '{{ $t("dialog.user.info.instance_game_version") }} {{ gameServerVersion }}
' + @@ -1093,6 +1094,8 @@ speechSynthesis.getVoices(); '{{ occupants }}/{{ capacity }}' + '({{ friendcount }})' + '{{ $t("dialog.user.info.instance_full") }}' + + '{{ $t("dialog.user.info.instance_closed") }}' + + '{{ $t("dialog.user.info.instance_hard_closed") }}' + '{{ $t("dialog.user.info.instance_queue") }} {{ queueSize }}' + '
', props: { @@ -1105,19 +1108,24 @@ speechSynthesis.getVoices(); return { isValidInstance: this.isValidInstance, isFull: this.isFull, + isClosed: this.isClosed, + isHardClosed: this.isHardClosed, occupants: this.occupants, capacity: this.capacity, queueSize: this.queueSize, queueEnabled: this.queueEnabled, platforms: this.platforms, userList: this.userList, - gameServerVersion: this.gameServerVersion + gameServerVersion: this.gameServerVersion, + canCloseInstance: this.canCloseInstance }; }, methods: { parse() { this.isValidInstance = false; this.isFull = false; + this.isClosed = false; + this.isHardClosed = false; this.occupants = 0; this.capacity = 0; this.queueSize = 0; @@ -1125,6 +1133,7 @@ speechSynthesis.getVoices(); this.platforms = []; this.userList = []; this.gameServerVersion = ''; + this.canCloseInstance = false; if ( !this.location || !this.instance || @@ -1136,6 +1145,13 @@ speechSynthesis.getVoices(); this.isFull = typeof this.instance.hasCapacityForYou !== 'undefined' && !this.instance.hasCapacityForYou; + if (this.instance.closedAt) { + if (this.instance.hardClose) { + this.isHardClosed = true; + } else { + this.isClosed = true; + } + } this.occupants = this.instance.n_users; if (this.location === $app.lastLocation.location) { // use gameLog for occupants when in same location @@ -1150,6 +1166,18 @@ speechSynthesis.getVoices(); if (this.instance.users) { this.userList = this.instance.users; } + if (this.instance.ownerId === API.currentUser.id) { + this.canCloseInstance = true; + } + if (this.instance.ownerId.startsWith('grp_')) { + // check group perms + var groupId = this.instance.ownerId; + var group = API.cachedGroups.get(groupId); + this.canCloseInstance = $app.hasGroupPermission( + group, + 'group-instance-moderate' + ); + } }, showUserDialog(userId) { API.$emit('SHOW_USER_DIALOG', userId); @@ -2604,6 +2632,8 @@ speechSynthesis.getVoices(); queueSize: 0, // only present when queuing is enabled platforms: [], gameServerVersion: 0, + hardClose: null, // boolean or null + closedAt: null, // string or null secureName: '', shortName: '', world: {}, @@ -4998,6 +5028,20 @@ speechSynthesis.getVoices(); } break; + case 'instance-closed': + // TODO: get worldName, groupName, hardClose + var noty = { + type: 'instance.closed', + location: content.instanceLocation, + message: 'Instance Closed', + created_at: new Date().toJSON() + }; + $app.notifyMenu('notification'); + $app.queueNotificationNoty(noty); + $app.notificationTable.data.push(noty); + $app.updateSharedFeed(true); + break; + default: console.log('Unknown pipeline type', args.json); } @@ -6572,6 +6616,9 @@ speechSynthesis.getVoices(); case 'group.queueReady': this.speak(noty.message); break; + case 'instance.closed': + this.speak(noty.message); + break; case 'PortalSpawn': if (noty.displayName) { this.speak( @@ -6794,6 +6841,9 @@ speechSynthesis.getVoices(); case 'group.queueReady': AppApi.XSNotification('VRCX', noty.message, timeout, image); break; + case 'instance.closed': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; case 'PortalSpawn': if (noty.displayName) { AppApi.XSNotification( @@ -7070,6 +7120,13 @@ speechSynthesis.getVoices(); image ); break; + case 'instance.closed': + AppApi.DesktopNotification( + 'Instance Closed', + noty.message, + image + ); + break; case 'PortalSpawn': if (noty.displayName) { AppApi.DesktopNotification( @@ -14994,6 +15051,7 @@ speechSynthesis.getVoices(); 'group.invite': 'On', 'group.joinRequest': 'Off', 'group.queueReady': 'On', + 'instance.closed': 'On', PortalSpawn: 'Everyone', Event: 'On', External: 'On', @@ -15032,6 +15090,7 @@ speechSynthesis.getVoices(); 'group.invite': 'On', 'group.joinRequest': 'On', 'group.queueReady': 'On', + 'instance.closed': 'On', PortalSpawn: 'Everyone', Event: 'On', External: 'On', @@ -15081,6 +15140,10 @@ speechSynthesis.getVoices(); $app.data.sharedFeedFilters.noty['group.queueReady'] = 'On'; $app.data.sharedFeedFilters.wrist['group.queueReady'] = 'On'; } + if (!$app.data.sharedFeedFilters.noty['instance.closed']) { + $app.data.sharedFeedFilters.noty['instance.closed'] = 'On'; + $app.data.sharedFeedFilters.wrist['instance.closed'] = 'On'; + } if (!$app.data.sharedFeedFilters.noty.External) { $app.data.sharedFeedFilters.noty.External = 'On'; $app.data.sharedFeedFilters.wrist.External = 'On'; @@ -27089,6 +27152,7 @@ speechSynthesis.getVoices(); groupName, worldName }; + this.notifyMenu('notification'); this.queueNotificationNoty(noty); this.notificationTable.data.push(noty); this.updateSharedFeed(true); @@ -29961,7 +30025,6 @@ speechSynthesis.getVoices(); }; // #endregion - // #region | V-Bucks API.$on('VBUCKS', function (args) { @@ -29984,6 +30047,63 @@ speechSynthesis.getVoices(); API.getVbucks(); }; + // #endregion + // #region | Close instance + + $app.methods.closeInstance = function (location) { + this.$confirm( + 'Continue? Close Instance, nobody will be able to join', + 'Confirm', + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'warning', + callback: (action) => { + if (action !== 'confirm') { + return; + } + API.closeInstance({ location, hardClose: false }); + } + } + ); + }; + + /** + * @param {{ + location: string, + hardClose: boolean + }} params + * @returns {Promise<{json: any, params}>} + */ + API.closeInstance = function (params) { + return this.call(`instances/${params.location}`, { + method: 'DELETE', + params: { + hardClose: params.hardClose ?? false + } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('INSTANCE:CLOSE', args); + return args; + }); + }; + + API.$on('INSTANCE:CLOSE', function (args) { + if (args.json) { + $app.$message({ + message: 'Instance closed', + type: 'success' + }); + + this.$emit('INSTANCE', { + json: args.json + }); + } + }); + // #endregion $app = new Vue($app); diff --git a/html/src/index.pug b/html/src/index.pug index 1389f0ac7..c851bada8 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1858,6 +1858,11 @@ html el-radio-group(v-model="sharedFeedFilters.noty['group.queueReady']" 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 Closed + 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") @@ -2080,6 +2085,11 @@ html el-radio-group(v-model="sharedFeedFilters.wrist['group.queueReady']" 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 Closed + 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") diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json index 6f9163d46..6246a4f12 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -608,7 +608,10 @@ "copy_url": "Copy URL", "copy_display_name": "Copy DisplayName", "accuracy_notice": "Info from local database may not be accurate", - "instance_full": "full" + "instance_full": "full", + "instance_closed": "closed", + "instance_hard_closed": "hard closed", + "close_instance": "Close Instance" }, "groups": { "header": "Groups", @@ -1570,7 +1573,7 @@ "status": "Status", "language": "Language", "bioLink": "Bio Links", - "joinCount": "Join Counts", + "joinCount": "Join Count", "timeTogether": "Time Together", "lastSeen": "Last Seen", "lastActivity": "Last Activity", diff --git a/html/src/mixins/tabs/notifications.pug b/html/src/mixins/tabs/notifications.pug index 48388b389..e5ce1ca4b 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', 'group.queueReady', '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', '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") @@ -20,7 +20,7 @@ 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-else-if="scope.row.type === 'group.queueReady'" placement="top") + el-tooltip(v-else-if="scope.row.type === 'group.queueReady' || scope.row.type === 'instance.closed'" 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)") @@ -85,7 +85,7 @@ mixin notificationsTab() 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.type !== 'requestInviteResponse' && scope.row.type !== 'inviteResponse' && scope.row.type !== 'message' && !scope.row.type.includes('group.') && !scope.row.type.includes('moderation.')") + 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.') && !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/vr.js b/html/src/vr.js index fbf105965..d07eee95d 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -454,10 +454,11 @@ Vue.component('marquee-text', MarqueeText); if (dev[0] === 'headset') return 0; if (dev[0] === 'leftController') return 1; if (dev[0] === 'rightController') return 2; - if (dev[0].toLowerCase().includes('controller')) return 3; + if (dev[0].toLowerCase().includes('controller')) + return 3; if (dev[0] === 'tracker' || dev[0] === 'base') return 4; return 5; - } + }; deviceList.sort((a, b) => deviceValue(a) - deviceValue(b)); deviceList.sort((a, b) => { if (a[1] === b[1]) { @@ -487,7 +488,9 @@ Vue.component('marquee-text', MarqueeText); } if (this.config.pcUptimeOnFeed) { AppApiVr.GetUptime().then((uptime) => { - this.pcUptime = timeToText(uptime); + if (uptime) { + this.pcUptime = timeToText(uptime); + } }); } else { this.pcUptime = ''; @@ -594,6 +597,9 @@ Vue.component('marquee-text', MarqueeText); case 'group.queueReady': text = noty.message; break; + case 'instance.closed': + text = noty.message; + break; case 'PortalSpawn': if (noty.displayName) { text = `${ diff --git a/html/src/vr.pug b/html/src/vr.pug index ca1ef0b68..7592922c6 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 === 'instance.closed'" 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 @@ -354,6 +359,11 @@ html span.extra span.time {{ feed.created_at | formatDate }} | #[span.name(v-text="feed.message")] + div(v-else-if="feed.type === 'instance.closed'" 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