diff --git a/html/src/app.js b/html/src/app.js index 4d5aad90..2e1b6f96 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -677,6 +677,7 @@ speechSynthesis.getVoices(); hiddenId: null, privateId: null, friendsId: null, + groupId: null, canRequestInvite: false, strict: false }; @@ -716,6 +717,8 @@ speechSynthesis.getVoices(); ctx.canRequestInvite = true; } else if (key === 'region') { ctx.region = value; + } else if (key === 'group') { + ctx.groupId = value; } else if (key === 'strict') { ctx.strict = true; } @@ -741,6 +744,9 @@ speechSynthesis.getVoices(); // FriendsOfGuests ctx.accessType = 'friends+'; ctx.userId = ctx.hiddenId; + } else if (ctx.groupId !== null) { + // Group + ctx.accessType = 'group'; } } else { ctx.worldId = _tag; @@ -2676,7 +2682,7 @@ speechSynthesis.getVoices(); }; var count = 50; // 5000 max for (var i = 0; i < count; i++) { - var args = await this.getGroupNotifications(params); + var args = await this.getNotificationsV2(params); $app.unseenNotifications = []; params.offset += 100; if (args.json.length < 100) { @@ -2769,12 +2775,13 @@ speechSynthesis.getVoices(); API.$on('NOTIFICATION:V2:LIST', function (args) { for (var json of args.json) { - this.$emit('NOTIFICATION:V2', json); + this.$emit('NOTIFICATION:V2', {json}); } console.log('NOTIFICATION:V2:LIST', args); }); - API.$on('NOTIFICATION:V2', function (json) { + API.$on('NOTIFICATION:V2', function (args) { + var json = args.json; json.created_at = json.createdAt; this.$emit('NOTIFICATION', { json, @@ -10856,6 +10863,9 @@ speechSynthesis.getVoices(); case 'friends+': L.accessName = `Friends+ #${L.instanceName} (${platform})`; break; + case 'group': + L.accessName = `Group #${L.instanceName} (${platform})`; + break; } } this.lastLocation$ = L; @@ -16318,6 +16328,7 @@ speechSynthesis.getVoices(); userId: '', accessType: '', region: '', + groupId: '', strict: false, location: '', shortName: '', @@ -16353,6 +16364,9 @@ speechSynthesis.getVoices(); if (D.accessType === 'invite+') { tags.push('~canRequestInvite'); } + if (D.accessType === 'group') { + tags.push(`~group(${D.groupId})`); + } } if (D.region === 'US West') { tags.push(`~region(us)`); @@ -16439,6 +16453,10 @@ speechSynthesis.getVoices(); this.newInstanceDialog.userId ); } + configRepository.setString( + 'instanceDialogGroupId', + this.newInstanceDialog.groupId + ); configRepository.setBool( 'instanceDialogStrict', this.newInstanceDialog.strict @@ -16451,6 +16469,7 @@ speechSynthesis.getVoices(); $app.watch['newInstanceDialog.accessType'] = saveNewInstanceDialog; $app.watch['newInstanceDialog.region'] = saveNewInstanceDialog; $app.watch['newInstanceDialog.userId'] = saveNewInstanceDialog; + $app.watch['newInstanceDialog.groupId'] = saveNewInstanceDialog; $app.watch['newInstanceDialog.strict'] = saveNewInstanceDialog; $app.methods.showNewInstanceDialog = function (tag) { @@ -16481,6 +16500,9 @@ speechSynthesis.getVoices(); if (configRepository.getString('instanceDialogUserId') !== null) { D.userId = configRepository.getString('instanceDialogUserId'); } + if (configRepository.getString('instanceDialogGroupId') !== null) { + D.groupId = configRepository.getString('instanceDialogGroupId'); + } D.strict = false; // if (configRepository.getBool('instanceDialogStrict') !== null) { // D.strict = configRepository.getBool('instanceDialogStrict'); @@ -22340,7 +22362,16 @@ speechSynthesis.getVoices(); }; // App: ChatBox Blacklist - $app.data.chatboxBlacklist = ['NP: ', 'Now Playing', '( ▶️ ']; + $app.data.chatboxBlacklist = [ + 'NP: ', + 'Now Playing', + 'Now playing', + "▶️ '", + '( ▶️ ', + "' - '", + "' by '", + '[Spotify] ' + ]; if (configRepository.getString('VRCX_chatboxBlacklist')) { $app.data.chatboxBlacklist = JSON.parse( configRepository.getString('VRCX_chatboxBlacklist') @@ -22692,10 +22723,11 @@ speechSynthesis.getVoices(); userId: string, groupId: string, params: { - visibility: string + visibility: string, + isSubscribedToAnnouncements: bool } */ - API.setGroupVisibility = function (userId, groupId, params) { + API.setGroupProps = function (userId, groupId, params) { return this.call(`groups/${groupId}/members/${userId}`, { method: 'PUT', params @@ -22706,16 +22738,18 @@ speechSynthesis.getVoices(); groupId, params }; - this.$emit('GROUP:VISIBILITY', args); + this.$emit('GROUP:PROPS', args); return args; }); }; - API.$on('GROUP:VISIBILITY', function (args) { - console.log('VISIBILITY', args); + API.$on('GROUP:PROPS', function (args) { + console.log('GROUP:PROPS', args); var json = args.json; if ($app.groupDialog.visible && $app.groupDialog.id === json.groupId) { $app.groupDialog.ref.myMember.visibility = json.visibility; + $app.groupDialog.ref.myMember.isSubscribedToAnnouncements = + json.isSubscribedToAnnouncements; } json.$memberId = json.id; json.id = json.groupId; @@ -22729,6 +22763,28 @@ speechSynthesis.getVoices(); }); }); + /* + params: { + groupId: string + } + */ + API.getGroupAnnouncement = function (params) { + return this.call(`groups/${params.groupId}/announcement`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:ANNOUNCEMENT', args); + return args; + }); + }; + + API.$on('GROUP:ANNOUNCEMENT', function (args) { + console.log('GROUP:ANNOUNCEMENT', args); + }); + /* params: { groupId: string @@ -22843,7 +22899,9 @@ speechSynthesis.getVoices(); id: '', inGroup: false, ownerDisplayName: '', - ref: {} + announcementDisplayName: '', + ref: {}, + announcement: {} }; $app.methods.showGroupDialog = function (groupId) { @@ -22857,7 +22915,9 @@ speechSynthesis.getVoices(); D.id = groupId; D.inGroup = false; D.ownerDisplayName = ''; + D.announcementDisplayName = ''; D.treeData = []; + D.announcement = {}; API.getCachedGroup({ groupId }) @@ -22871,24 +22931,10 @@ speechSynthesis.getVoices(); throw err; }) .then((args) => { - if (D.id === args.ref.id) { + if (groupId === args.ref.id) { D.loading = false; D.ref = args.ref; D.inGroup = args.ref.membershipStatus === 'member'; - if (args.cache) { - API.getGroup(args.params) - .catch((err) => { - throw err; - }) - .then((args1) => { - if (D.id === args1.ref.id) { - D.ref = args1.ref; - D.inGroup = - args.ref.membershipStatus === 'member'; - } - return args1; - }); - } D.ownerDisplayName = args.ref.ownerId; API.getCachedUser({ userId: args.ref.ownerId @@ -22896,10 +22942,46 @@ speechSynthesis.getVoices(); D.ownerDisplayName = args1.ref.displayName; return args1; }); + if (args.cache) { + this.getGroupDialogGroup(groupId); + } } }); }; + $app.methods.getGroupDialogGroup = function (groupId) { + var D = this.groupDialog; + return API.getGroup({groupId}) + .catch((err) => { + throw err; + }) + .then((args1) => { + if (D.id === args1.ref.id) { + D.ref = args1.ref; + D.inGroup = args1.ref.membershipStatus === 'member'; + if (D.inGroup) { + API.getGroupAnnouncement({ + groupId + }).then((args2) => { + if (groupId === args2.json.groupId) { + D.announcement = args2.json; + if (D.announcement && D.announcement.authorId) { + API.getCachedUser({ + userId: D.announcement.authorId + }).then((args3) => { + D.announcementDisplayName = + args3.ref.displayName; + return args3; + }); + } + } + }); + } + } + return args1; + }); + }; + $app.methods.groupDialogCommand = function (command) { var D = this.groupDialog; if (D.visible === false) { @@ -22921,6 +23003,12 @@ speechSynthesis.getVoices(); case 'Visibility Hidden': this.setGroupVisibility(D.id, 'hidden'); break; + case 'Subscribe To Announcements': + this.setGroupSubscription(D.id, true); + break; + case 'Unsubscribe To Announcements': + this.setGroupSubscription(D.id, false); + break; } }; @@ -22969,8 +23057,26 @@ speechSynthesis.getVoices(); }; $app.methods.setGroupVisibility = function (groupId, visibility) { - return API.setGroupVisibility(API.currentUser.id, groupId, { + return API.setGroupProps(API.currentUser.id, groupId, { visibility + }).then((args) => { + this.$message({ + message: 'Group visibility updated', + type: 'success' + }); + return args; + }); + }; + + $app.methods.setGroupSubscription = function (groupId, subscribe) { + return API.setGroupProps(API.currentUser.id, groupId, { + isSubscribedToAnnouncements: subscribe + }).then((args) => { + this.$message({ + message: 'Group subscription updated', + type: 'success' + }); + return args; }); }; diff --git a/html/src/index.pug b/html/src/index.pug index 77a96b5b..ded5fbe1 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -2162,6 +2162,7 @@ html div span(v-if="groupDialog.ref.ownerId === API.currentUser.id" style="margin-right:5px") 👑 span.dialog-title(v-text="groupDialog.ref.name" style="margin-right:5px") + span(style="color:#909399;font-family:monospace;font-size:12px;margin-right:5px") {{ groupDialog.ref.shortCode }}.{{ groupDialog.ref.discriminator }} el-tooltip(v-for="item in groupDialog.ref.$languages" :key="item.key" placement="top") template(#content) span {{ item.value }} ({{ item.key }}) @@ -2172,13 +2173,22 @@ html el-tag(v-if="groupDialog.ref.isVerified" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Verified el-tag(v-if="groupDialog.ref.privacy === 'private'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Private - el-tag(v-else-if="groupDialog.ref.joinState === 'open'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Public - el-tag(v-else-if="groupDialog.ref.joinState === 'request'" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Invite - el-tag(v-else-if="groupDialog.ref.joinState === 'invite'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Closed + el-tag(v-if="groupDialog.ref.privacy === 'default'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Public + + el-tag(v-if="groupDialog.ref.joinState === 'open'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Open + el-tag(v-else-if="groupDialog.ref.joinState === 'request'" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Request + el-tag(v-else-if="groupDialog.ref.joinState === 'invite'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Invite + el-tag(v-else-if="groupDialog.ref.joinState === 'closed'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Closed el-tag(v-if="groupDialog.inGroup" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Joined - el-tag(v-if="groupDialog.ref.myMember && groupDialog.ref.myMember.bannedAt" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Banned + + template(v-if="groupDialog.inGroup && groupDialog.ref.myMember") + el-tag(v-if="groupDialog.ref.myMember.visibility === 'visible'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Visible + el-tag(v-else-if="groupDialog.ref.myMember.visibility === 'friends'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Friends + el-tag(v-else-if="groupDialog.ref.myMember.visibility === 'hidden'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Hidden + el-tag(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Subscribed + div(style="margin-top:5px") span(v-show="groupDialog.ref.name !== groupDialog.ref.description" v-text="groupDialog.ref.description" style="font-size:12px") div(style="flex:none;margin-left:10px") @@ -2190,6 +2200,8 @@ html el-button(type="default" icon="el-icon-star-off" circle @click="setGroupRepresentation(groupDialog.id)" style="margin-left:5px" :disabled="groupDialog.ref.privacy === 'private'") el-tooltip(v-else-if="groupDialog.ref.membershipStatus === 'requested'" placement="top" content="Cancel join request" :disabled="hideTooltips") el-button(type="default" icon="el-icon-close" circle @click="cancelGroupRequest(groupDialog.id)" style="margin-left:5px") + el-tooltip(v-else-if="groupDialog.ref.membershipStatus === 'invited'" placement="top" content="Pending invite" :disabled="hideTooltips") + el-button(type="default" icon="el-icon-message" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") template(v-else) el-tooltip(v-if="groupDialog.ref.joinState === 'request'" placement="top" content="Request to join" :disabled="hideTooltips") el-button(type="default" icon="el-icon-message" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") @@ -2203,30 +2215,44 @@ html el-dropdown-menu(#default="dropdown") el-dropdown-item(icon="el-icon-refresh" command="Refresh") Refresh - template(v-if="groupDialog.ref.myMember") - 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 - el-dropdown-item(icon="el-icon-view" command="Visibility Hidden") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'hidden'")] Visibility Hidden + template(v-if="groupDialog.inGroup") + 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 + + 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 + el-dropdown-item(icon="el-icon-view" command="Visibility Hidden") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'hidden'")] Visibility Hidden - el-dropdown-item(v-if="groupDialog.inGroup" icon="el-icon-delete" command="Leave Group" style="color:#F56C6C" divided) Leave Group + el-dropdown-item(icon="el-icon-delete" command="Leave Group" style="color:#F56C6C" divided) Leave Group el-tabs el-tab-pane(label="Info") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="openExternalLink(groupDialog.ref.bannerUrl)") .x-friend-list(style="max-height:none") - .x-friend-item(style="width:350px;cursor:default") + .x-friend-item(v-if="groupDialog.ref.membershipStatus === 'member'" style="width:100%;cursor:default") .detail - span.name Group URL - span.extra {{ groupDialog.ref.$url }} - el-tooltip(placement="top" content="Copy URL to clipboard" :disabled="hideTooltips") - el-button(type="default" @click="copyGroupUrl(groupDialog.ref.$url)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") - .x-friend-item(style="width:350px;cursor:default") + span.name Announcement + span(style="display:block" v-text="groupDialog.announcement.title") + div(v-if="groupDialog.announcement.imageUrl" style="display:inline-block;margin-right:5px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="groupDialog.announcement.imageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + img.x-link(v-lazy="groupDialog.announcement.imageUrl" style="height:500px" @click="openExternalLink(groupDialog.announcement.imageUrl)") + pre.extra(style="display:inline-block;vertical-align:top;font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0") {{ groupDialog.announcement.text || '-' }} + br + .extra(v-if="groupDialog.announcement.id" style="float:right;margin-left:5px") + el-tooltip(placement="bottom") + template(#content) + span {{ groupDialog.announcement.updatedAt | formatDate('long') }} + span(@click="showUserDialog(groupDialog.announcement.authorId)" style="cursor:pointer") + span(v-text="groupDialog.announcementDisplayName" style="margin-right:5px") + timer(:epoch="Date.parse(groupDialog.announcement.updatedAt)") + .x-friend-item(style="width:100%;cursor:default") .detail - span.name Group ID - span.extra {{ groupDialog.id }} - el-tooltip(placement="top" content="Copy ID to clipboard" :disabled="hideTooltips") - el-button(type="default" @click="copyGroupId(groupDialog.id)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") + span.name Rules + pre.extra(style="font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0 0.5em 0 0") {{ groupDialog.ref.rules || '-' }} .x-friend-item(style="cursor:default") .detail span.name Members @@ -2244,24 +2270,24 @@ html span(v-text="link") img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)") .extra(v-else) - - .x-friend-item(style="width:100%;cursor:default") + .x-friend-item(style="width:350px;cursor:default") .detail - span.name Rules - pre.extra(style="font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0 0.5em 0 0") {{ groupDialog.ref.rules || '-' }} + span.name Group URL + span.extra {{ groupDialog.ref.$url }} + el-tooltip(placement="top" content="Copy URL to clipboard" :disabled="hideTooltips") + el-button(type="default" @click="copyGroupUrl(groupDialog.ref.$url)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") + .x-friend-item(style="width:350px;cursor:default") + .detail + span.name Group ID + span.extra {{ groupDialog.id }} + el-tooltip(placement="top" content="Copy ID to clipboard" :disabled="hideTooltips") + el-button(type="default" @click="copyGroupId(groupDialog.id)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") div(v-if="groupDialog.ref.membershipStatus === 'member'" style="width:100%;margin-top:10px;border-top:1px solid #e4e7ed14") div(style="width:100%;display:flex;margin-top:10px") .x-friend-item(style="cursor:default") .detail span.name Joined At span.extra {{ groupDialog.ref.myMember.joinedAt | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name Announcements - span.extra {{ groupDialog.ref.myMember.isSubscribedToAnnouncements }} - .x-friend-item(style="cursor:default") - .detail - span.name Visibility - span.extra {{ groupDialog.ref.myMember.visibility }} el-tab-pane(label="JSON") el-button(type="default" @click="refreshGroupDialogTreeData()" size="mini" icon="el-icon-refresh" circle) el-tree(:data="groupDialog.treeData" style="margin-top:5px;font-size:12px") @@ -2382,6 +2408,7 @@ html el-form-item(label="Access Type") el-radio-group(v-model="newInstanceDialog.accessType" size="mini" @change="buildInstance") el-radio-button(label="public") + el-radio-button(label="group") el-radio-button(label="friends+") el-radio-button(label="friends") el-radio-button(label="invite+") @@ -2398,7 +2425,7 @@ html el-input(v-model="newInstanceDialog.worldId" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()") el-form-item(label="Instance ID") el-input(v-model="newInstanceDialog.instanceName" placeholder="Random" size="mini") - el-form-item(label="Instance Creator") + el-form-item(label="Instance Creator" v-if="newInstanceDialog.accessType !== 'public' && newInstanceDialog.accessType !== 'group'") el-select(v-model="newInstanceDialog.userId" clearable placeholder="Choose User" filterable style="width:100%") el-option-group(v-if="API.currentUser" label="ME") el-option.x-friend-item(:label="API.currentUser.displayName" :value="API.currentUser.id" style="height:auto") @@ -2438,6 +2465,8 @@ html .detail span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") 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") el-input(v-model="newInstanceDialog.location" size="mini" readonly) el-form-item(label="URL") diff --git a/html/src/vr.js b/html/src/vr.js index 19e7f717..14d02896 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -219,6 +219,7 @@ Vue.component('marquee-text', MarqueeText); hiddenId: null, privateId: null, friendsId: null, + groupId: null, canRequestInvite: false, strict: false }; @@ -230,6 +231,15 @@ Vue.component('marquee-text', MarqueeText); ctx.isTraveling = true; } else if (_tag.startsWith('local') === false) { var sep = _tag.indexOf(':'); + // technically not part of instance id, but might be there when coping id from url so why not support it + var shortNameQualifier = '&shortName='; + var shortNameIndex = _tag.indexOf(shortNameQualifier); + if (shortNameIndex >= 0) { + ctx.shortName = _tag.substr( + shortNameIndex + shortNameQualifier.length + ); + _tag = _tag.substr(0, shortNameIndex); + } if (sep >= 0) { ctx.worldId = _tag.substr(0, sep); ctx.instanceId = _tag.substr(sep + 1); @@ -249,6 +259,8 @@ Vue.component('marquee-text', MarqueeText); ctx.canRequestInvite = true; } else if (key === 'region') { ctx.region = value; + } else if (key === 'group') { + ctx.groupId = value; } else if (key === 'strict') { ctx.strict = true; } @@ -274,6 +286,9 @@ Vue.component('marquee-text', MarqueeText); // FriendsOfGuests ctx.accessType = 'friends+'; ctx.userId = ctx.hiddenId; + } else if (ctx.groupId !== null) { + // Group + ctx.accessType = 'group'; } } else { ctx.worldId = _tag;