Files
VRCX/src/stores/group.js
2026-01-11 06:05:10 +13:00

1136 lines
35 KiB
JavaScript

import { nextTick, reactive, ref, watch } from 'vue';
import { ElMessageBox } from 'element-plus';
import { defineStore } from 'pinia';
import { toast } from 'vue-sonner';
import {
groupRequest,
instanceRequest,
userRequest,
worldRequest
} from '../api';
import {
convertFileUrlToImageUrl,
hasGroupPermission,
replaceBioSymbols
} from '../shared/utils';
import { database } from '../service/database.js';
import { groupDialogFilterOptions } from '../shared/constants/';
import { useGameStore } from './game';
import { useInstanceStore } from './instance';
import { useNotificationStore } from './notification';
import { useUserStore } from './user';
import { watchState } from '../service/watchState';
import configRepository from '../service/config';
import * as workerTimers from 'worker-timers';
export const useGroupStore = defineStore('Group', () => {
const instanceStore = useInstanceStore();
const gameStore = useGameStore();
const userStore = useUserStore();
const notificationStore = useNotificationStore();
let cachedGroups = new Map();
const groupDialog = ref({
visible: false,
loading: false,
isGetGroupDialogGroupLoading: false,
treeData: [],
id: '',
inGroup: false,
ownerDisplayName: '',
ref: {},
announcement: {},
posts: [],
postsFiltered: [],
calendar: [],
members: [],
memberSearch: '',
memberSearchResults: [],
instances: [],
memberRoles: [],
lastVisit: '',
memberFilter: {
name: 'dialog.group.members.filters.everyone',
id: null
},
memberSortOrder: {
name: 'dialog.group.members.sorting.joined_at_desc',
value: 'joinedAt:desc'
},
postsSearch: '',
galleries: {}
});
const currentUserGroups = reactive(new Map());
const inviteGroupDialog = ref({
visible: false,
loading: false,
groupId: '',
groupName: '',
userId: '',
userIds: [],
userObject: {
id: '',
displayName: '',
$userColour: ''
}
});
const moderateGroupDialog = ref({
visible: false,
groupId: '',
groupName: '',
userId: '',
userObject: {}
});
const groupMemberModeration = ref({
visible: false,
loading: false,
id: '',
groupRef: {},
auditLogTypes: [],
openWithUserId: ''
});
const inGameGroupOrder = ref([]);
const groupInstances = ref([]);
const currentUserGroupsInit = ref(false);
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
groupDialog.value.visible = false;
inviteGroupDialog.value.visible = false;
moderateGroupDialog.value.visible = false;
groupMemberModeration.value.visible = false;
currentUserGroupsInit.value = false;
cachedGroups.clear();
currentUserGroups.clear();
if (isLoggedIn) {
initUserGroups();
}
},
{ flush: 'sync' }
);
function showGroupDialog(groupId) {
if (!groupId) {
return;
}
const D = groupDialog.value;
D.visible = true;
D.loading = true;
D.id = groupId;
D.inGroup = false;
D.ownerDisplayName = '';
D.treeData = [];
D.announcement = {};
D.posts = [];
D.postsFiltered = [];
D.instances = [];
D.memberRoles = [];
D.lastVisit = '';
D.memberSearch = '';
D.memberSearchResults = [];
D.galleries = {};
D.members = [];
D.memberFilter = groupDialogFilterOptions.everyone;
D.calendar = [];
groupRequest
.getCachedGroup({
groupId
})
.catch((err) => {
D.loading = false;
D.visible = false;
toast.error('Failed to load group');
throw err;
})
.then((args) => {
if (groupId === args.ref.id) {
D.loading = false;
D.ref = args.ref;
D.inGroup = args.ref.membershipStatus === 'member';
D.ownerDisplayName = args.ref.ownerId;
userRequest
.getCachedUser({
userId: args.ref.ownerId
})
.then((args1) => {
D.ownerDisplayName = args1.ref.displayName;
return args1;
});
database.getLastGroupVisit(D.ref.name).then((r) => {
if (D.id === args.ref.id) {
D.lastVisit = r.created_at;
}
});
instanceStore.applyGroupDialogInstances();
getGroupDialogGroup(groupId);
}
});
}
/**
*
* @param {object }ref
* @param {string} oldUserId
* @param {string} newUserId
* @returns {Promise<void>}
*/
async function groupOwnerChange(ref, oldUserId, newUserId) {
const oldUser = await userRequest.getCachedUser({
userId: oldUserId
});
const newUser = await userRequest.getCachedUser({
userId: newUserId
});
const oldDisplayName = oldUser?.ref?.displayName;
const newDisplayName = newUser?.ref?.displayName;
groupChange(
ref,
`Owner changed from ${oldDisplayName} to ${newDisplayName}`
);
}
function groupChange(ref, message) {
if (!currentUserGroupsInit.value) {
return;
}
// oh the level of cursed for compibility
const json = {
id: Math.random().toString(36),
type: 'groupChange',
senderUserId: ref.id,
senderUsername: ref.name,
imageUrl: ref.iconUrl,
details: {
imageUrl: ref.iconUrl
},
message,
created_at: new Date().toJSON()
};
notificationStore.handleNotification({
json,
params: {
notificationId: json.id
}
});
// delay to wait for json to be assigned to ref
workerTimers.setTimeout(saveCurrentUserGroups, 100);
}
function saveCurrentUserGroups() {
if (!currentUserGroupsInit.value) {
return;
}
const groups = [];
for (const ref of currentUserGroups.values()) {
groups.push({
id: ref.id,
name: ref.name,
ownerId: ref.ownerId,
iconUrl: ref.iconUrl,
roles: ref.roles,
roleIds: ref.myMember?.roleIds
});
}
configRepository.setString(
`VRCX_currentUserGroups_${userStore.currentUser.id}`,
JSON.stringify(groups)
);
}
/**
*
* @param {object} ref
* @param {array} oldRoles
* @param {array} newRoles
* @param {array} oldRoleIds
* @param {array} newRoleIds
*/
function groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) {
// check for removed/added roleIds
for (const roleId of oldRoleIds) {
if (!newRoleIds.includes(roleId)) {
let roleName = '';
const role = oldRoles.find(
(fineRole) => fineRole.id === roleId
);
if (role) {
roleName = role.name;
}
groupChange(ref, `Role ${roleName} removed`);
}
}
if (typeof newRoles !== 'undefined') {
for (const roleId of newRoleIds) {
if (!oldRoleIds.includes(roleId)) {
let roleName = '';
const role = newRoles.find(
(fineRole) => fineRole.id === roleId
);
if (role) {
roleName = role.name;
}
groupChange(ref, `Role ${roleName} added`);
}
}
}
}
/**
*
* @param {object} ref
*/
function applyPresenceGroups(ref) {
if (!currentUserGroupsInit.value) {
// wait for init before diffing
return;
}
const groups = ref.presence?.groups;
if (!groups) {
console.error('applyPresenceGroups: invalid groups', ref);
return;
}
if (groups.length === 0) {
// as it turns out, this is not the most trust worthly source of info
return;
}
// update group list
for (const groupId of groups) {
if (!currentUserGroups.has(groupId)) {
onGroupJoined(groupId);
}
}
for (const groupId of currentUserGroups.keys()) {
if (!groups.includes(groupId)) {
onGroupLeft(groupId);
}
}
}
/**
*
* @param {string} groupId
*/
function onGroupJoined(groupId) {
if (!currentUserGroups.has(groupId)) {
currentUserGroups.set(groupId, {
id: groupId,
name: '',
iconUrl: ''
});
groupRequest
.getGroup({ groupId, includeRoles: true })
.then((args) => {
applyGroup(args.json);
saveCurrentUserGroups();
return args;
});
}
}
/**
*
* @param {string} groupId
*/
async function onGroupLeft(groupId) {
const args = await groupRequest.getGroup({ groupId });
const ref = applyGroup(args.json);
if (ref.membershipStatus === 'member') {
// wtf, not trusting presence
console.error(
`onGroupLeft: presence lied, still a member of ${groupId}`
);
return;
}
if (groupDialog.value.visible && groupDialog.value.id === groupId) {
showGroupDialog(groupId);
}
if (currentUserGroups.has(groupId)) {
currentUserGroups.delete(groupId);
groupChange(ref, 'Left group');
// delay to wait for json to be assigned to ref
workerTimers.setTimeout(saveCurrentUserGroups, 100);
}
}
/**
*
* @param {{ groupId: string }} params
* @return { Promise<{posts: any, params}> }
*/
async function getAllGroupPosts(params) {
const n = 100;
let posts = [];
let offset = 0;
let total = 0;
const args = await groupRequest.getGroupPosts({
groupId: params.groupId,
n,
offset
});
do {
posts = posts.concat(args.json.posts);
total = args.json.total;
offset += n;
} while (offset < total);
const returnArgs = {
posts,
params
};
const D = groupDialog.value;
if (D.id === args.params.groupId) {
for (const post of args.json.posts) {
post.title = replaceBioSymbols(post.title);
post.text = replaceBioSymbols(post.text);
}
if (args.json.posts.length > 0) {
D.announcement = args.json.posts[0];
}
D.posts = args.json.posts;
updateGroupPostSearch();
}
return returnArgs;
}
function getGroupDialogGroup(groupId) {
const D = groupDialog.value;
D.isGetGroupDialogGroupLoading = false;
return groupRequest
.getGroup({ groupId, includeRoles: true })
.catch((err) => {
throw err;
})
.then((args) => {
const ref = applyGroup(args.json);
if (D.id === ref.id) {
D.ref = ref;
D.inGroup = ref.membershipStatus === 'member';
for (const role of ref.roles) {
if (
D.ref &&
D.ref.myMember &&
Array.isArray(D.ref.myMember.roleIds) &&
D.ref.myMember.roleIds.includes(role.id)
) {
D.memberRoles.push(role);
}
}
getAllGroupPosts({
groupId
});
D.isGetGroupDialogGroupLoading = true;
groupRequest
.getGroupInstances({
groupId
})
.then((args) => {
if (groupDialog.value.id === args.params.groupId) {
instanceStore.applyGroupDialogInstances(
args.json.instances
);
}
for (const json of args.json.instances) {
instanceStore.applyInstance(json);
worldRequest
.getCachedWorld({
worldId: json.world.id
})
.then((args1) => {
json.world = args1.ref;
});
// get queue size etc
instanceRequest.getInstance({
worldId: json.worldId,
instanceId: json.instanceId
});
}
});
groupRequest.getGroupCalendar(groupId).then((args) => {
if (groupDialog.value.id === args.params.groupId) {
D.calendar = args.json.results;
for (const event of D.calendar) {
applyGroupEvent(event);
// fetch again for isFollowing
groupRequest
.getGroupCalendarEvent({
groupId,
eventId: event.id
})
.then((args) => {
Object.assign(
event,
applyGroupEvent(args.json)
);
});
}
}
});
}
nextTick(() => (D.isGetGroupDialogGroupLoading = false));
return args;
});
}
function applyGroupEvent(event) {
return {
userInterest: {
createdAt: null,
isFollowing: false,
updatedAt: null
},
...event,
title: replaceBioSymbols(event.title),
description: replaceBioSymbols(event.description)
};
}
async function updateInGameGroupOrder() {
inGameGroupOrder.value = [];
try {
const json = await gameStore.getVRChatRegistryKey(
`VRC_GROUP_ORDER_${userStore.currentUser.id}`
);
if (!json) {
return;
}
inGameGroupOrder.value = JSON.parse(json);
} catch (err) {
console.error(err);
}
}
function sortGroupInstancesByInGame(a, b) {
const aIndex = inGameGroupOrder.value.indexOf(a?.group?.id);
const bIndex = inGameGroupOrder.value.indexOf(b?.group?.id);
if (aIndex === -1 && bIndex === -1) {
return 0;
}
if (aIndex === -1) {
return 1;
}
if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
}
function leaveGroup(groupId) {
groupRequest
.leaveGroup({
groupId
})
.then((args) => {
const groupId = args.params.groupId;
if (
groupDialog.value.visible &&
groupDialog.value.id === groupId
) {
groupDialog.value.inGroup = false;
getGroupDialogGroup(groupId);
}
if (
userStore.userDialog.visible &&
userStore.userDialog.id === userStore.currentUser.id &&
userStore.userDialog.representedGroup.id === groupId
) {
getCurrentUserRepresentedGroup();
}
});
}
function leaveGroupPrompt(groupId) {
ElMessageBox.confirm(
'Are you sure you want to leave this group?',
'Confirm',
{
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info'
}
)
.then(() => {
leaveGroup(groupId);
})
.catch(() => {});
}
function updateGroupPostSearch() {
const D = groupDialog.value;
const search = D.postsSearch.toLowerCase();
D.postsFiltered = D.posts.filter((post) => {
if (search === '') {
return true;
}
if (post.title.toLowerCase().includes(search)) {
return true;
}
if (post.text.toLowerCase().includes(search)) {
return true;
}
return false;
});
}
function setGroupVisibility(groupId, visibility) {
return groupRequest
.setGroupMemberProps(userStore.currentUser.id, groupId, {
visibility
})
.then((args) => {
handleGroupMemberProps(args);
toast.success('Group visibility updated');
return args;
});
}
function setGroupSubscription(groupId, subscribe) {
return groupRequest
.setGroupMemberProps(userStore.currentUser.id, groupId, {
isSubscribedToAnnouncements: subscribe
})
.then((args) => {
handleGroupMemberProps(args);
toast.success('Group subscription updated');
return args;
});
}
/**
*
* @param {object} json
* @returns {object} ref
*/
function applyGroup(json) {
let ref = cachedGroups.get(json.id);
if (json.rules) {
json.rules = replaceBioSymbols(json.rules);
}
if (json.name) {
json.name = replaceBioSymbols(json.name);
}
if (json.description) {
json.description = replaceBioSymbols(json.description);
}
if (typeof ref === 'undefined') {
ref = {
id: '',
name: '',
shortCode: '',
description: '',
bannerId: '',
bannerUrl: '',
createdAt: '',
discriminator: '',
galleries: [],
iconId: '',
iconUrl: '',
isVerified: false,
joinState: '',
languages: [],
links: [],
memberCount: 0,
memberCountSyncedAt: '',
membershipStatus: '',
onlineMemberCount: 0,
ownerId: '',
privacy: '',
rules: null,
tags: [],
// in group
initialRoleIds: [],
myMember: {
bannedAt: null,
groupId: '',
has2FA: false,
id: '',
isRepresenting: false,
isSubscribedToAnnouncements: false,
joinedAt: '',
managerNotes: '',
membershipStatus: '',
permissions: [],
roleIds: [],
userId: '',
visibility: '',
_created_at: '',
_id: '',
_updated_at: ''
},
updatedAt: '',
// includeRoles: true
roles: [],
// group list
$memberId: '',
groupId: '',
isRepresenting: false,
memberVisibility: false,
mutualGroup: false,
// VRCX
$languages: [],
...json
};
cachedGroups.set(ref.id, ref);
} else {
if (currentUserGroups.has(ref.id)) {
// compare group props
if (
ref.ownerId &&
json.ownerId &&
ref.ownerId !== json.ownerId
) {
// owner changed
groupOwnerChange(json, ref.ownerId, json.ownerId);
}
if (ref.name && json.name && ref.name !== json.name) {
// name changed
groupChange(
json,
`Name changed from ${ref.name} to ${json.name}`
);
}
if (ref.myMember?.roleIds && json.myMember?.roleIds) {
const oldRoleIds = ref.myMember.roleIds;
const newRoleIds = json.myMember.roleIds;
if (
oldRoleIds.length !== newRoleIds.length ||
!oldRoleIds.every(
(value, index) => value === newRoleIds[index]
)
) {
// roleIds changed
groupRoleChange(
json,
ref.roles,
json.roles,
oldRoleIds,
newRoleIds
);
}
}
}
if (json.myMember) {
if (typeof json.myMember.roleIds === 'undefined') {
// keep roleIds
json.myMember.roleIds = ref.myMember.roleIds;
}
Object.assign(ref.myMember, json.myMember);
}
Object.assign(ref, json);
}
// update myMember without fetching member
if (typeof json.memberVisibility !== 'undefined') {
ref.myMember.visibility = json.memberVisibility;
}
if (typeof json.isRepresenting !== 'undefined') {
ref.myMember.isRepresenting = json.isRepresenting;
}
if (typeof json.membershipStatus !== 'undefined') {
ref.myMember.membershipStatus = json.membershipStatus;
}
if (typeof json.roleIds !== 'undefined') {
ref.myMember.roleIds = json.roleIds;
}
ref.$url = `https://vrc.group/${ref.shortCode}.${ref.discriminator}`;
applyGroupLanguage(ref);
const currentUserGroupRef = currentUserGroups.get(ref.id);
if (currentUserGroupRef) {
currentUserGroups.set(ref.id, ref);
}
const D = groupDialog.value;
if (D.visible && D.id === ref.id) {
D.inGroup = ref.membershipStatus === 'member';
D.ref = ref;
}
return ref;
}
function handleGroupRepresented(args) {
const D = userStore.userDialog;
const json = args.json;
D.representedGroup = json;
D.representedGroup.$thumbnailUrl = convertFileUrlToImageUrl(
json.iconUrl
);
if (!json || !json.isRepresenting) {
D.isRepresentedGroupLoading = false;
}
if (!json.groupId) {
// no group
return;
}
if (args.params.userId !== userStore.currentUser.id) {
// not current user, don't apply someone elses myMember
return;
}
json.$memberId = json.id;
json.id = json.groupId;
applyGroup(json);
}
function handleGroupList(args) {
for (const json of args.json) {
json.$memberId = json.id;
json.id = json.groupId;
applyGroup(json);
}
}
function handleGroupMemberProps(args) {
if (args.userId === userStore.currentUser.id) {
const json = args.json;
json.$memberId = json.id;
json.id = json.groupId;
if (
groupDialog.value.visible &&
groupDialog.value.id === json.groupId
) {
groupDialog.value.ref.myMember.visibility = json.visibility;
groupDialog.value.ref.myMember.isSubscribedToAnnouncements =
json.isSubscribedToAnnouncements;
}
if (
userStore.userDialog.visible &&
userStore.userDialog.id === userStore.currentUser.id
) {
getCurrentUserRepresentedGroup();
}
handleGroupMember({
json,
params: {
groupId: json.groupId
}
});
}
let member;
if (groupDialog.value.id === args.json.groupId) {
let i;
for (i = 0; i < groupDialog.value.members.length; ++i) {
member = groupDialog.value.members[i];
if (member.userId === args.json.userId) {
Object.assign(member, applyGroupMember(args.json));
break;
}
}
for (i = 0; i < groupDialog.value.memberSearchResults.length; ++i) {
member = groupDialog.value.memberSearchResults[i];
if (member.userId === args.json.userId) {
Object.assign(member, applyGroupMember(args.json));
break;
}
}
}
}
function handleGroupPermissions(args) {
if (args.params.userId !== userStore.currentUser.id) {
return;
}
const json = args.json;
for (const groupId in json) {
const permissions = json[groupId];
const group = cachedGroups.get(groupId);
if (group) {
group.myMember.permissions = permissions;
}
}
}
/**
*
* @param {object} args
*/
function handleGroupPost(args) {
const D = groupDialog.value;
if (D.id !== args.params.groupId) {
return;
}
const newPost = args.json;
newPost.title = replaceBioSymbols(newPost.title);
newPost.text = replaceBioSymbols(newPost.text);
let hasPost = false;
// update existing post
for (const post of D.posts) {
if (post.id === newPost.id) {
Object.assign(post, newPost);
hasPost = true;
break;
}
}
// set or update announcement
if (newPost.id === D.announcement.id || !D.announcement.id) {
D.announcement = newPost;
}
// add new post
if (!hasPost) {
D.posts.unshift(newPost);
}
updateGroupPostSearch();
}
function handleGroupMember(args) {
args.ref = applyGroupMember(args.json);
}
async function handleGroupUserInstances(args) {
groupInstances.value = [];
for (const json of args.json.instances) {
if (args.json.fetchedAt) {
// tack on fetchedAt
json.$fetchedAt = args.json.fetchedAt;
}
const instanceRef = instanceStore.applyInstance(json);
const groupRef = cachedGroups.get(json.ownerId);
if (typeof groupRef === 'undefined') {
if (watchState.isFriendsLoaded) {
const args = await groupRequest.getGroup({
groupId: json.ownerId
});
applyGroup(args.json);
}
return;
}
groupInstances.value.push({
group: groupRef,
instance: instanceRef
});
}
}
/**
*
* @param {object} json
* @returns {*}
*/
function applyGroupMember(json) {
let ref;
if (typeof json?.user !== 'undefined') {
if (json.userId === userStore.currentUser.id) {
json.user = userStore.currentUser;
json.$displayName = userStore.currentUser.displayName;
} else {
ref = userStore.cachedUsers.get(json.user.id);
if (typeof ref !== 'undefined') {
json.user = ref;
json.$displayName = ref.displayName;
} else {
json.$displayName = json.user?.displayName;
}
}
}
// update myMember without fetching member
if (json?.userId === userStore.currentUser.id) {
ref = cachedGroups.get(json.groupId);
if (typeof ref !== 'undefined') {
const newJson = {
id: json.groupId,
memberVisibility: json.visibility,
isRepresenting: json.isRepresenting,
isSubscribedToAnnouncements:
json.isSubscribedToAnnouncements,
joinedAt: json.joinedAt,
roleIds: json.roleIds,
membershipStatus: json.membershipStatus
};
applyGroup(newJson);
}
}
return json;
}
function applyGroupLanguage(ref) {
ref.$languages = [];
const { languages } = ref;
if (!languages) {
return;
}
for (const language of languages) {
const value = userStore.subsetOfLanguages[language];
if (typeof value === 'undefined') {
continue;
}
ref.$languages.push({
key: language,
value
});
}
}
async function loadCurrentUserGroups(userId, groups) {
const savedGroups = JSON.parse(
await configRepository.getString(
`VRCX_currentUserGroups_${userId}`,
'[]'
)
);
cachedGroups.clear();
currentUserGroups.clear();
for (const group of savedGroups) {
const json = {
id: group.id,
name: group.name,
iconUrl: group.iconUrl,
ownerId: group.ownerId,
roles: group.roles,
myMember: {
roleIds: group.roleIds
}
};
const ref = applyGroup(json);
currentUserGroups.set(group.id, ref);
}
if (groups) {
const promises = groups.map(async (groupId) => {
const groupRef = cachedGroups.get(groupId);
if (
typeof groupRef !== 'undefined' &&
groupRef.roles?.length > 0
) {
return;
}
try {
console.log(`Fetching group with missing roles ${groupId}`);
const args = await groupRequest.getGroup({
groupId,
includeRoles: true
});
const ref = applyGroup(args.json);
currentUserGroups.set(groupId, ref);
} catch (err) {
console.error(err);
}
});
await Promise.allSettled(promises);
}
currentUserGroupsInit.value = true;
getCurrentUserGroups();
}
async function getCurrentUserGroups() {
const args = await groupRequest.getGroups({
userId: userStore.currentUser.id
});
handleGroupList(args);
currentUserGroups.clear();
for (const group of args.json) {
const ref = applyGroup(group);
if (!currentUserGroups.has(group.id)) {
currentUserGroups.set(group.id, ref);
}
}
const args1 = await groupRequest.getGroupPermissions({
userId: userStore.currentUser.id
});
handleGroupPermissions(args1);
saveCurrentUserGroups();
}
function getCurrentUserRepresentedGroup() {
return groupRequest
.getRepresentedGroup({
userId: userStore.currentUser.id
})
.then((args) => {
handleGroupRepresented(args);
return args;
});
}
async function initUserGroups() {
updateInGameGroupOrder();
loadCurrentUserGroups(
userStore.currentUser.id,
userStore.currentUser?.presence?.groups
);
}
function showModerateGroupDialog(userId) {
const D = moderateGroupDialog.value;
D.userId = userId;
D.userObject = {};
D.visible = true;
}
function showGroupMemberModerationDialog(groupId, userId = '') {
const D = groupMemberModeration.value;
D.id = groupId;
D.openWithUserId = userId;
D.groupRef = {};
D.auditLogTypes = [];
groupRequest.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
if (hasGroupPermission(D.groupRef, 'group-audit-view')) {
groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => {
if (D.id !== args.params.groupId) {
return;
}
D.auditLogTypes = args.json;
});
}
});
D.visible = true;
}
return {
groupDialog,
currentUserGroups,
inviteGroupDialog,
moderateGroupDialog,
groupMemberModeration,
cachedGroups,
inGameGroupOrder,
groupInstances,
currentUserGroupsInit,
initUserGroups,
showGroupDialog,
applyGroup,
saveCurrentUserGroups,
applyPresenceGroups,
getGroupDialogGroup,
updateInGameGroupOrder,
sortGroupInstancesByInGame,
leaveGroup,
leaveGroupPrompt,
updateGroupPostSearch,
setGroupVisibility,
setGroupSubscription,
applyGroupMember,
loadCurrentUserGroups,
handleGroupPost,
handleGroupUserInstances,
handleGroupMember,
handleGroupPermissions,
handleGroupMemberProps,
handleGroupList,
handleGroupRepresented,
showModerateGroupDialog,
showGroupMemberModerationDialog,
onGroupLeft,
applyGroupEvent
};
});