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