diff --git a/AppApi.cs b/AppApi.cs index 647c8596..e63ad649 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -22,6 +22,7 @@ using System.Net.Sockets; using System.Text; using System.Collections.Generic; using System.Threading; +using System.IO.Pipes; namespace VRCX { @@ -320,6 +321,16 @@ namespace VRCX return false; } + public void IPCAnnounceStart() + { + var ipcClient = new NamedPipeClientStream(".", "vrcx-ipc", PipeDirection.InOut); + ipcClient.Connect(); + if (!ipcClient.IsConnected) + return; + var buffer = Encoding.UTF8.GetBytes($"{{\"type\":\"VRCXLaunch\"}}" + (char)0x00); + ipcClient.BeginWrite(buffer, 0, buffer.Length, IPCClient.OnSend, ipcClient); + } + public void ExecuteAppFunction(string function, string json) { if (MainForm.Instance != null) diff --git a/html/src/app.js b/html/src/app.js index 73ca3ac8..a15306b0 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -2565,7 +2565,7 @@ speechSynthesis.getVoices(); if ( ref.type === 'friendRequest' || ref.type === 'hiddenFriendRequest' || - ref.type === 'group.invite' + ref.type.includes('.') ) { for (var i = array.length - 1; i >= 0; i--) { if (array[i].id === ref.id) { @@ -8017,6 +8017,7 @@ speechSynthesis.getVoices(); $app.migrateMemos(); $app.migrateFriendLog(args.json.id); } + await AppApi.IPCAnnounceStart(); }); $app.methods.loadPlayerList = function () { @@ -8941,7 +8942,6 @@ speechSynthesis.getVoices(); $app.data.photonLobbyWatcherLoop = false; $app.data.photonLobbyTimeout = []; $app.data.photonLobbyJointime = new Map(); - $app.data.photonLobbyBots = []; $app.data.photonEvent7List = new Map(); $app.data.photonLastEvent7List = ''; $app.data.photonLastChatBoxMsg = new Map(); @@ -9053,7 +9053,6 @@ speechSynthesis.getVoices(); $app.methods.photonLobbyWatcherLoopStop = function () { this.photonLobbyWatcherLoop = false; this.photonLobbyTimeout = []; - this.photonLobbyBots = []; AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); }; @@ -9140,56 +9139,9 @@ speechSynthesis.getVoices(); this.photonLobbyTimeout = hudTimeout; this.getCurrentInstanceUserList(); } - this.photonBotCheck(dtNow); workerTimers.setTimeout(() => this.photonLobbyWatcher(), 500); }; - $app.methods.photonBotCheck = function (dtNow) { - if (this.photonLobbyCurrentUser === 0) { - return; - } - var photonBots = []; - this.photonLobbyCurrent.forEach((ref, id) => { - if (this.photonLobbyJointime.has(id)) { - var {joinTime, hasInstantiated, avatarEyeHeight} = - this.photonLobbyJointime.get(id); - } - var text = ''; - if (avatarEyeHeight < 0) { - text = 'Photon bot has joined, invalid avatarEyeHeight'; - } else if ( - joinTime && - joinTime + 11000 < dtNow && - !hasInstantiated - ) { - text = 'User failed to instantiate after 10 seconds'; - } - if (text && id !== this.photonLobbyCurrentUser) { - if (!this.photonLobbyBots.includes(id)) { - this.addEntryPhotonEvent({ - photonId: id, - text, - type: 'PhotonBot', - color: 'yellow', - created_at: new Date().toJSON() - }); - var entry = { - created_at: new Date().toJSON(), - type: 'Event', - data: `${text} ${this.getDisplayNameFromPhotonId( - id - )} (${this.getUserIdFromPhotonId(id)})` - }; - this.queueGameLogNoty(entry); - this.addGameLog(entry); - database.addGamelogEventToDatabase(entry); - } - photonBots.unshift(id); - } - }); - this.photonLobbyBots = photonBots; - }; - $app.data.photonEventTableFilter = ''; $app.data.photonEventTableTypeFilter = []; $app.data.photonEventTableTypeOverlayFilter = []; @@ -9922,7 +9874,6 @@ speechSynthesis.getVoices(); } if (data.avatarEyeHeight < 0) { text = 'Photon bot has joined, invalid avatarEyeHeight'; - this.photonLobbyBots.unshift(photonId); } else if (data.user.last_platform === 'android' && !data.inVRMode) { var text = 'User joined as Quest in desktop mode'; } else if ( @@ -12057,7 +12008,7 @@ speechSynthesis.getVoices(); if ( ref.type !== 'friendRequest' && ref.type !== 'hiddenFriendRequest' && - ref.type !== 'group.invite' + !ref.type.includes('.') ) { database.addNotificationToDatabase(ref); } @@ -15078,6 +15029,8 @@ speechSynthesis.getVoices(); this.showGalleryDialog(); } else if (command === 'Copy User') { this.copyUser(D.id); + } else if (command === 'Invite To Group') { + this.showInviteGroupDialog('', D.id); } else { this.$confirm(`Continue? ${command}`, 'Confirm', { confirmButtonText: 'Confirm', @@ -20483,6 +20436,9 @@ speechSynthesis.getVoices(); AppApi.FocusWindow(); this.eventLaunchCommand(data.command); break; + case 'VRCXLaunch': + console.log('VRCXLaunch:', data); + break; default: console.log('IPC:', data); } @@ -21203,7 +21159,7 @@ speechSynthesis.getVoices(); $app.data.databaseVersion = configRepository.getInt('VRCX_databaseVersion'); $app.methods.updateDatabaseVersion = async function () { - var databaseVersion = 1; + var databaseVersion = 2; if (this.databaseVersion !== databaseVersion) { console.log( `Updating database from ${this.databaseVersion} to ${databaseVersion}...` @@ -21212,6 +21168,7 @@ speechSynthesis.getVoices(); await database.fixGameLogTraveling(); // fix bug with gameLog location being set as traveling await database.fixNegativeGPS(); // fix GPS being a negative value due to VRCX bug with traveling await database.fixBrokenLeaveEntries(); // fix user instance timer being higher than current user location timer + await database.fixBrokenGroupInvites(); // fix notification v2 in wrong table this.databaseVersion = databaseVersion; configRepository.setInt('VRCX_databaseVersion', databaseVersion); console.log('Database update complete.'); @@ -22590,6 +22547,7 @@ speechSynthesis.getVoices(); }); if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { $app.groupDialog.inGroup = json.membershipStatus === 'member'; + this.getGroup({groupId}); } }); @@ -22615,6 +22573,14 @@ speechSynthesis.getVoices(); var groupId = args.params.groupId; if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { $app.groupDialog.inGroup = false; + this.getGroup({groupId}); + } + if ( + $app.userDialog.visible && + $app.userDialog.id === this.currentUser.id && + $app.userDialog.representedGroup.id === groupId + ) { + $app.getCurrentUserRepresentedGroup(); } }); @@ -22672,12 +22638,7 @@ speechSynthesis.getVoices(); $app.userDialog.visible && $app.userDialog.id === this.currentUser.id ) { - this.getRepresentedGroup({userId: this.currentUser.id}).then( - (args1) => { - $app.userDialog.representedGroup = args1.json; - return args1; - } - ); + $app.getCurrentUserRepresentedGroup(); } }); @@ -22749,12 +22710,7 @@ speechSynthesis.getVoices(); $app.userDialog.visible && $app.userDialog.id === this.currentUser.id ) { - this.getRepresentedGroup({userId: this.currentUser.id}).then( - (args1) => { - $app.userDialog.representedGroup = args1.json; - return args1; - } - ); + $app.getCurrentUserRepresentedGroup(); } this.$emit('GROUP', { json, @@ -22838,6 +22794,28 @@ speechSynthesis.getVoices(); args.ref = this.applyGroupMember(args.json); }); + /* + params: { + groupId: string, + userId: string + } + */ + API.sendGroupInvite = function (params) { + return this.call(`groups/${params.groupId}/invites`, { + method: 'POST', + params: { + userId: params.userId + } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITE', args); + return args; + }); + }; + /* params: { groupId: string @@ -23101,6 +23079,9 @@ speechSynthesis.getVoices(); case 'Unsubscribe To Announcements': this.setGroupSubscription(D.id, false); break; + case 'Invite To Group': + this.showInviteGroupDialog(D.id, ''); + break; } }; @@ -23147,6 +23128,10 @@ speechSynthesis.getVoices(); }); }; + API.$on('LOGOUT', function () { + $app.groupDialog.visible = false; + }); + $app.methods.leaveGroup = function (groupId) { return API.leaveGroup({ groupId @@ -23286,6 +23271,128 @@ speechSynthesis.getVoices(); return false; }; + $app.methods.getCurrentUserRepresentedGroup = function () { + return API.getRepresentedGroup({ + userId: API.currentUser.id + }).then((args) => { + this.userDialog.representedGroup = args.json; + return args; + }); + }; + + // group invite users + + $app.data.inviteGroupDialog = { + visible: false, + loading: false, + groupId: '', + groupName: '', + userId: '', + userIds: [], + userObject: {}, + groups: [] + }; + + $app.methods.showInviteGroupDialog = function (groupId, userId) { + this.$nextTick(() => adjustDialogZ(this.$refs.inviteGroupDialog.$el)); + var D = this.inviteGroupDialog; + D.userIds = []; + D.groups = []; + D.groupId = groupId; + D.groupName = groupId; + D.userId = userId; + D.userObject = {}; + D.visible = true; + D.loading = true; + if (groupId) { + API.getCachedGroup({ + groupId + }) + .then((args) => { + D.groupName = args.ref.name; + }) + .catch(() => { + D.groupId = ''; + }); + this.isAllowedToInviteToGroup(); + } + API.getGroups({n: 100, userId: API.currentUser.id}).then((args) => { + this.inviteGroupDialog.groups = args.json; + D.loading = false; + }); + + if (userId) { + API.getCachedUser({userId}).then((args) => { + D.userObject = args.ref; + }); + D.userIds = [userId]; + } + }; + + API.$on('LOGOUT', function () { + $app.inviteGroupDialog.visible = false; + }); + + $app.methods.sendGroupInvite = function () { + this.$confirm('Continue? Invite To Group', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + var D = this.inviteGroupDialog; + if (action !== 'confirm' || D.loading === true) { + return; + } + D.loading = true; + var inviteLoop = () => { + if (D.userIds.length > 0) { + var receiverUserId = D.userIds.shift(); + API.sendGroupInvite({ + groupId: D.groupId, + userId: receiverUserId + }).finally(inviteLoop); + } else { + D.loading = false; + } + }; + inviteLoop(); + } + }); + }; + + $app.methods.isAllowedToInviteToGroup = function () { + var D = this.inviteGroupDialog; + var groupId = D.groupId; + if (!groupId) { + return; + } + D.loading = true; + API.getGroup({groupId}) + .then((args) => { + var group = args.ref; + if (group.joinState === 'open') { + return args; + } + if ( + group.myMember && + group.myMember.permissions && + group.myMember.permissions.includes('group-invites-manage') + ) { + return args; + } + // not allowed to invite + D.groupId = ''; + this.$message({ + type: 'error', + message: 'You are not allowed to invite to this group' + }); + return args; + }) + .finally(() => { + D.loading = false; + }); + }; + $app = new Vue($app); window.$app = $app; })(); diff --git a/html/src/index.pug b/html/src/index.pug index 65569b62..c8a30bcf 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -807,11 +807,10 @@ html el-button(type="text" icon="el-icon-close" size="mini" @click="sendNotificationResponse(scope.row.id, scope.row.link, 'decline')") el-tooltip(placement="top" content="Block Group" :disabled="hideTooltips") el-button(type="text" icon="el-icon-circle-close" size="mini" @click="sendNotificationResponse(scope.row.id, scope.row.link, 'block')") - - template(v-if="scope.row.type !== 'requestInviteResponse' && scope.row.type !== 'inviteResponse' && scope.row.type !== 'message' && scope.row.type !== 'group.invite'") + 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 !== 'friendRequest' && scope.row.type !== 'hiddenFriendRequest' && scope.row.type !== 'group.invite'") + 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)") @@ -1633,6 +1632,7 @@ html template(v-if="lastLocation.location && isGameRunning && checkCanInvite(lastLocation.location)") el-dropdown-item(icon="el-icon-message" command="Invite") Invite el-dropdown-item(icon="el-icon-message" command="Invite Message") Invite With Message + el-dropdown-item(icon="el-icon-message" command="Invite To Group") Invite To Group template(v-else-if="userDialog.incomingRequest") el-dropdown-item(icon="el-icon-check" command="Accept Friend Request") Accept Friend Request el-dropdown-item(icon="el-icon-close" command="Decline Friend Request") Decline Friend Request @@ -2222,7 +2222,7 @@ html template(v-if="groupDialog.ref.myMember") el-dropdown-item(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" icon="el-icon-close" command="Unsubscribe To Announcements" divided) Unsubscribe To Announcements el-dropdown-item(v-else icon="el-icon-check" command="Subscribe To Announcements" divided) Subscribe To Announcements - + el-dropdown-item(icon="el-icon-message" command="Invite To Group") Invite To Group template(v-if="groupDialog.ref.myMember && groupDialog.ref.privacy === 'default'") el-dropdown-item(icon="el-icon-view" command="Visibility Everyone" divided) #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'visible'")] Visibility Everyone el-dropdown-item(icon="el-icon-view" command="Visibility Friends") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'friends'")] Visibility Friends @@ -2476,15 +2476,15 @@ html 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="OFFLINE") + span(v-else v-text="friend.id") + el-option-group(v-if="friendsGroup3.length" label="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") + span(v-else v-text="friend.id") el-form-item(label="Group ID" v-if="newInstanceDialog.accessType === 'group'") el-input(v-model="newInstanceDialog.groupId" placeholder="grp_UUID" size="mini") el-form-item(label="Location") @@ -3228,7 +3228,7 @@ html el-button(type="default" size="small" @click="displayGalleryUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") Upload el-button(type="default" size="small" @click="setProfilePicOverride('')" icon="el-icon-close" :disabled="!API.currentUser.profilePicOverride") Clear br - .x-friend-item(v-for="image in galleryTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in galleryTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setProfilePicOverride(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentProfilePic(image.id) }") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") div(style="float:right;margin-top:5px") @@ -3243,7 +3243,7 @@ html el-button(type="default" size="small" @click="displayVRCPlusIconUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") Upload el-button(type="default" size="small" @click="setVRCPlusIcon('')" icon="el-icon-close" :disabled="!API.currentUser.userIcon") Clear br - .x-friend-item(v-for="image in VRCPlusIconsTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in VRCPlusIconsTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setVRCPlusIcon(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentVRCPlusIcon(image.id) }") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") div(style="float:right;margin-top:5px") @@ -3564,6 +3564,61 @@ html el-tag(v-for="user in chatboxUserBlacklist" type="info" disable-transitions="true" :key="user[0]" style="margin-right:5px;margin-top:5px" closable @close="deleteChatboxUserBlacklist(user[0])") span {{user[1]}} + //- dialog: invite group + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="inviteGroupDialog" :visible.sync="inviteGroupDialog.visible" title="Invite To Group" width="450px") + div(v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading") + span The limits for this is unknown be careful using it + el-select(v-model="inviteGroupDialog.groupId" clearable placeholder="Choose Group" filterable :disabled="inviteGroupDialog.loading" @change="isAllowedToInviteToGroup" style="margin-top:15px") + el-option-group(v-if="inviteGroupDialog.groups.length" label="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") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + el-select(v-model="inviteGroupDialog.userIds" multiple clearable placeholder="Choose Friends" filterable :disabled="inviteGroupDialog.loading" style="width:100%;margin-top:15px") + el-option-group(v-if="inviteGroupDialog.userId" label="Selected User") + el-option.x-friend-item(:key="inviteGroupDialog.userObject.id" :label="inviteGroupDialog.userObject.displayName" :value="inviteGroupDialog.userObject.id" style="height:auto") + template(v-if="inviteGroupDialog.userObject.id") + .avatar(:class="userStatusClass(inviteGroupDialog.userObject)") + img(v-lazy="userImage(inviteGroupDialog.userObject)") + .detail + span.name(v-text="inviteGroupDialog.userObject.displayName" :style="{'color':inviteGroupDialog.userObject.$userColour}") + span(v-else v-text="inviteGroupDialog.userId") + el-option-group(v-if="friendsGroup0.length" label="VIP") + 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="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="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="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") + template(#footer) + el-button(type="primary" size="small" :disabled="inviteGroupDialog.loading || !inviteGroupDialog.userIds.length" @click="sendGroupInvite()") Invite + //- dialog: open source software notice el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" title="Open Source Software Notice" width="650px") div(v-if="ossDialog" style="height:350px;overflow:hidden scroll;word-break:break-all") diff --git a/html/src/repository/database.js b/html/src/repository/database.js index e7156595..2109f995 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -1750,6 +1750,12 @@ class Database { ); return userId; } + + async fixBrokenGroupInvites() { + await sqliteService.executeNonQuery( + `DELETE FROM ${Database.userPrefix}_notifications WHERE type LIKE '%.%'` + ); + } } var self = new Database();