Files
VRCX/src/stores/notification.js

2725 lines
89 KiB
JavaScript

import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import Noty from 'noty';
import dayjs from 'dayjs';
import {
checkCanInvite,
displayLocation,
escapeTag,
extractFileId,
extractFileVersion,
getUserMemo,
parseLocation,
removeFromArray,
replaceBioSymbols
} from '../shared/utils';
import {
friendRequest,
instanceRequest,
notificationRequest,
userRequest,
worldRequest
} from '../api';
import { database, dbVars } from '../service/database';
import { AppDebug } from '../service/appConfig';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useFavoriteStore } from './favorite';
import { useFriendStore } from './friend';
import { useGameStore } from './game';
import { useGeneralSettingsStore } from './settings/general';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance';
import { useLocationStore } from './location';
import { useModalStore } from './modal';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useUserStore } from './user';
import { useWristOverlaySettingsStore } from './settings/wristOverlay';
import { watchState } from '../service/watchState';
import configRepository from '../service/config';
export const useNotificationStore = defineStore('Notification', () => {
const { t } = useI18n();
const generalSettingsStore = useGeneralSettingsStore();
const locationStore = useLocationStore();
const favoriteStore = useFavoriteStore();
const friendStore = useFriendStore();
const notificationsSettingsStore = useNotificationsSettingsStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const appearanceSettingsStore = useAppearanceSettingsStore();
const userStore = useUserStore();
const wristOverlaySettingsStore = useWristOverlaySettingsStore();
const uiStore = useUiStore();
const gameStore = useGameStore();
const sharedFeedStore = useSharedFeedStore();
const instanceStore = useInstanceStore();
const modalStore = useModalStore();
const groupStore = useGroupStore();
const notificationInitStatus = ref(false);
const notificationTable = ref({
data: [],
filters: [
{
prop: 'type',
value: []
},
{
prop: ['senderUsername', 'message'],
value: ''
}
],
pageSize: 20,
pageSizeLinked: true,
paginationProps: {
layout: 'sizes,prev,pager,next,total'
}
});
const unseenNotifications = ref([]);
const isNotificationsLoading = ref(false);
const isNotificationCenterOpen = ref(false);
const FRIEND_TYPES = new Set([
'friendRequest',
'ignoredFriendRequest',
'invite',
'requestInvite',
'inviteResponse',
'requestInviteResponse',
'boop'
]);
const GROUP_TYPES_PREFIX = ['group.', 'moderation.'];
const GROUP_EXACT_TYPES = new Set(['groupChange', 'event.announcement']);
function getNotificationCategory(type) {
if (!type) return 'other';
if (FRIEND_TYPES.has(type)) return 'friend';
if (
GROUP_EXACT_TYPES.has(type) ||
GROUP_TYPES_PREFIX.some((p) => type.startsWith(p))
)
return 'group';
return 'other';
}
const friendNotifications = computed(() =>
notificationTable.value.data.filter(
(n) => getNotificationCategory(n.type) === 'friend'
)
);
const groupNotifications = computed(() =>
notificationTable.value.data.filter(
(n) => getNotificationCategory(n.type) === 'group'
)
);
const otherNotifications = computed(() =>
notificationTable.value.data.filter(
(n) => getNotificationCategory(n.type) === 'other'
)
);
const unseenSet = computed(() => new Set(unseenNotifications.value));
const unseenFriendNotifications = computed(() =>
friendNotifications.value.filter((n) => unseenSet.value.has(n.id))
);
const unseenGroupNotifications = computed(() =>
groupNotifications.value.filter((n) => unseenSet.value.has(n.id))
);
const unseenOtherNotifications = computed(() =>
otherNotifications.value.filter((n) => unseenSet.value.has(n.id))
);
const hasUnseenNotifications = computed(
() => unseenNotifications.value.length > 0
);
const notyMap = {};
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
isNotificationsLoading.value = false;
notificationTable.value.data = [];
if (isLoggedIn) {
initNotifications();
}
},
{ flush: 'sync' }
);
async function init() {
notificationTable.value.filters[0].value = JSON.parse(
await configRepository.getString(
'VRCX_notificationTableFilters',
'[]'
)
);
}
init();
function handleNotification(args) {
args.ref = applyNotification(args.json);
const { ref } = args;
const array = notificationTable.value.data;
const { length } = array;
if (ref.seen) {
removeFromArray(unseenNotifications.value, ref.id);
} else if (!unseenNotifications.value.includes(ref.id)) {
unseenNotifications.value.push(ref.id);
}
for (let i = 0; i < length; ++i) {
if (array[i].id === ref.id) {
array[i] = ref;
return;
}
}
if (ref.senderUserId !== userStore.currentUser.id) {
if (
ref.type !== 'friendRequest' &&
ref.type !== 'ignoredFriendRequest' &&
!ref.type.includes('.')
) {
database.addNotificationToDatabase(ref);
}
if (watchState.isFriendsLoaded && notificationInitStatus.value) {
if (
ref.details?.worldId &&
!instanceStore.cachedInstances.has(ref.details.worldId)
) {
// get instance name for invite
const L = parseLocation(ref.details.worldId);
if (L.isRealInstance) {
instanceRequest.getCachedInstance({
worldId: L.worldId,
instanceId: L.instanceId
});
}
}
if (
notificationTable.value.filters[0].value.length === 0 ||
notificationTable.value.filters[0].value.includes(ref.type)
) {
uiStore.notifyMenu('notification');
}
queueNotificationNoty(ref);
sharedFeedStore.addEntry(ref);
}
}
notificationTable.value.data.push(ref);
const D = userStore.userDialog;
if (
D.visible === false ||
ref.type !== 'friendRequest' ||
ref.senderUserId !== D.id
) {
return;
}
D.incomingRequest = true;
}
function handleNotificationHide(notificationId) {
const ref = notificationTable.value.data.find(
(n) => n.id === notificationId
);
if (typeof ref === 'undefined') {
return;
}
if (
ref.type === 'friendRequest' ||
ref.type === 'ignoredFriendRequest' ||
ref.type.includes('.')
) {
removeFromArray(notificationTable.value.data, ref);
} else {
ref.$isExpired = true;
database.updateNotificationExpired(ref);
}
handleNotificationExpire({
ref,
params: {
notificationId: ref.id
}
});
}
function handlePipelineNotification(args) {
const ref = args.json;
if (
ref.type !== 'requestInvite' ||
generalSettingsStore.autoAcceptInviteRequests === 'Off'
) {
return;
}
let currentLocation = locationStore.lastLocation.location;
if (locationStore.lastLocation.location === 'traveling') {
currentLocation = locationStore.lastLocationDestination;
}
if (!currentLocation) {
// game log disabled, use API location
currentLocation = userStore.currentUser.$locationTag;
if (userStore.currentUser.$travelingToLocation) {
currentLocation = userStore.currentUser.$travelingToLocation;
}
}
if (!currentLocation) {
return;
}
if (
generalSettingsStore.autoAcceptInviteRequests === 'All Favorites' &&
!favoriteStore.state.favoriteFriends_.some(
(x) => x.id === ref.senderUserId
)
) {
return;
}
if (
generalSettingsStore.autoAcceptInviteRequests ===
'Selected Favorites'
) {
const groups = generalSettingsStore.autoAcceptInviteGroups;
if (groups.length === 0) {
return;
} else {
let found = false;
for (const groupKey of groups) {
if (groupKey.startsWith('local:')) {
const localGroup = groupKey.slice(6);
const localFavs =
favoriteStore.localFriendFavorites.get(localGroup);
if (localFavs && localFavs.has(ref.senderUserId)) {
found = true;
break;
}
} else {
const remoteFavs =
favoriteStore.cachedFavorites.get(groupKey);
if (
remoteFavs &&
remoteFavs.some(
(f) => f.favoriteId === ref.senderUserId
)
) {
found = true;
break;
}
}
}
if (!found) {
return;
}
}
}
if (!checkCanInvite(currentLocation)) {
return;
}
const L = parseLocation(currentLocation);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args1) => {
notificationRequest
.sendInvite(
{
instanceId: L.tag,
worldId: L.tag,
worldName: args1.ref.name,
rsvp: true
},
ref.senderUserId
)
.then((_args) => {
const text = `Auto invite sent to ${ref.senderUsername}`;
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppDebug.errorNoty = new Noty({
type: 'info',
text
});
AppDebug.errorNoty.show();
console.log(text);
notificationRequest.hideNotification({
notificationId: ref.id
});
return _args;
})
.catch((err) => {
console.error(err);
});
});
}
function handleNotificationSee(notificationId) {
removeFromArray(unseenNotifications.value, notificationId);
if (unseenNotifications.value.length === 0) {
uiStore.removeNotify('notification');
}
const ref = notificationTable.value.data.find(
(n) => n.id === notificationId
);
if (ref) {
ref.seen = true;
}
database.seenNotificationV2(ref);
}
function handleNotificationAccept(args) {
let ref;
const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) {
if (array[i].id === args.params.notificationId) {
ref = array[i];
break;
}
}
if (typeof ref === 'undefined') {
return;
}
ref.$isExpired = true;
args.ref = ref;
handleNotificationExpire({
ref,
params: {
notificationId: ref.id
}
});
friendStore.handleFriendAdd({
params: {
userId: ref.senderUserId
}
});
const D = userStore.userDialog;
if (
D.visible === false ||
typeof args.ref === 'undefined' ||
args.ref.type !== 'friendRequest' ||
args.ref.senderUserId !== D.id
) {
return;
}
D.isFriend = true;
}
function handleNotificationExpire(args) {
const { ref } = args;
const D = userStore.userDialog;
if (
D.visible === false ||
ref.type !== 'friendRequest' ||
ref.senderUserId !== D.id
) {
return;
}
D.incomingRequest = false;
}
/**
*
* @param {object} data
* @returns {object}
*/
function applyNotification(data) {
const json = { ...data };
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
let ref;
const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) {
if (array[i].id === json.id) {
ref = array[i];
break;
}
}
// delete any null in json
for (const key in json) {
if (json[key] === null) {
delete json[key];
}
}
if (typeof ref === 'undefined') {
ref = {
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
// VRCX
$isExpired: false,
//
...json
};
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
if (ref.details !== Object(ref.details)) {
let details = {};
if (ref.details !== '{}') {
try {
const object = JSON.parse(ref.details);
if (object === Object(object)) {
details = object;
}
} catch (err) {
console.log(err);
}
}
ref.details = details;
}
return ref;
}
function applyNotificationV2(data) {
const json = { ...data };
// delete any null in json
for (const key in json) {
if (json[key] === null || typeof json[key] === 'undefined') {
delete json[key];
}
}
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
if (json.title) {
json.title = replaceBioSymbols(json.title);
}
let ref = notificationTable.value.data.find((n) => n.id === json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
data: {},
responses: [],
details: {},
version: 2,
...json
};
} else {
Object.assign(ref, json);
}
ref.created_at = ref.createdAt; // for table
// legacy handling of boops
if (ref.type === 'boop' && ref.title) {
ref.message = ref.title;
ref.title = '';
if (ref.details?.emojiId?.startsWith('default_')) {
ref.imageUrl = ref.details.emojiId;
ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
} else {
ref.imageUrl = `${AppDebug.endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
}
}
return ref;
}
function handleNotificationV2(args) {
const ref = applyNotificationV2(args.json);
if (ref.seen) {
removeFromArray(unseenNotifications.value, ref.id);
} else if (!unseenNotifications.value.includes(ref.id)) {
unseenNotifications.value.push(ref.id);
}
const existingNotification = notificationTable.value.data.find(
(n) => n.id === ref.id
);
if (existingNotification) {
Object.assign(existingNotification, ref);
database.addNotificationV2ToDatabase(existingNotification); // update
return;
}
if (
notificationTable.value.filters[0].value.length === 0 ||
notificationTable.value.filters[0].value.includes(ref.type)
) {
uiStore.notifyMenu('notification');
}
database.addNotificationV2ToDatabase(ref);
notificationTable.value.data.push(ref);
queueNotificationNoty(ref);
sharedFeedStore.addEntry(ref);
}
function handleNotificationV2Update(args) {
const notificationId = args.params.notificationId;
const json = { ...args.json };
if (!json) {
return;
}
json.id = notificationId;
handleNotificationV2({
json,
params: {
notificationId
}
});
if (json.seen) {
handleNotificationSee(notificationId);
}
}
function handleNotificationV2Hide(notificationId) {
database.expireNotificationV2(notificationId);
const ref = notificationTable.value.data.find(
(n) => n.id === notificationId
);
if (ref) {
ref.expiresAt = new Date().toJSON();
ref.seen = true;
}
}
function expireFriendRequestNotifications() {
const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) {
if (
array[i].type === 'friendRequest' ||
array[i].type === 'ignoredFriendRequest'
) {
array.splice(i, 1);
}
}
}
/**
*
* @param {string} notificationId
*/
function expireNotification(notificationId) {
let ref;
const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) {
if (array[i].id === notificationId) {
ref = array[i];
break;
}
}
if (typeof ref === 'undefined') {
return;
}
ref.$isExpired = true;
database.updateNotificationExpired(ref);
handleNotificationExpire({
ref,
params: {
notificationId: ref.id
}
});
}
/**
*
* @returns {Promise<void>}
*/
async function refreshNotifications() {
isNotificationsLoading.value = true;
let count;
let params;
try {
expireFriendRequestNotifications();
params = {
n: 100,
offset: 0
};
count = 50; // 5000 max
for (let i = 0; i < count; i++) {
const args = await notificationRequest.getNotifications(params);
for (const json of args.json) {
handleNotification({
json,
params: {
notificationId: json.id
}
});
}
params.offset += 100;
if (args.json.length < 100) {
break;
}
}
params = {
n: 100,
offset: 0
};
count = 50; // 5000 max
for (let i = 0; i < count; i++) {
const args =
await notificationRequest.getNotificationsV2(params);
for (const json of args.json) {
handleNotificationV2({
json,
params: {
notificationId: json.id
}
});
}
params.offset += 100;
if (args.json.length < 100) {
break;
}
}
params = {
n: 100,
offset: 0
};
count = 50; // 5000 max
for (let i = 0; i < count; i++) {
const args =
await notificationRequest.getHiddenFriendRequests(params);
for (const json of args.json) {
json.type = 'ignoredFriendRequest';
handleNotification({
json,
params: {
notificationId: json.id
}
});
}
params.offset += 100;
if (args.json.length < 100) {
break;
}
}
} catch (err) {
console.error(err);
} finally {
isNotificationsLoading.value = false;
notificationInitStatus.value = true;
}
}
/**
*
* @param {object} noty
*/
function queueNotificationNoty(noty) {
noty.isFriend = friendStore.friends.has(noty.senderUserId);
noty.isFavorite = friendStore.localFavoriteFriends.has(
noty.senderUserId
);
const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
if (
notyFilter[noty.type] &&
(notyFilter[noty.type] === 'On' ||
notyFilter[noty.type] === 'Friends' ||
(notyFilter[noty.type] === 'VIP' && noty.isFavorite))
) {
playNoty(noty);
}
}
function playNoty(noty) {
if (
userStore.currentUser.status === 'busy' ||
!watchState.isFriendsLoaded
) {
return;
}
let displayName = '';
if (noty.displayName) {
displayName = noty.displayName;
} else if (noty.senderUsername) {
displayName = noty.senderUsername;
} else if (noty.sourceDisplayName) {
displayName = noty.sourceDisplayName;
}
if (displayName) {
// don't play noty twice
const notyId = `${noty.type},${displayName}`;
if (notyMap[notyId] && notyMap[notyId] >= noty.created_at) {
return;
}
notyMap[notyId] = noty.created_at;
}
const bias = new Date(Date.now() - 60000).toJSON();
for (const [notyId, createdAt] of Object.entries(notyMap)) {
if (createdAt < bias) {
delete notyMap[notyId];
}
}
if (noty.created_at < bias) {
// don't play noty if it's over 1min old
return;
}
const notiConditions = {
Always: () => true,
'Inside VR': () => gameStore.isSteamVRRunning,
'Outside VR': () => !gameStore.isSteamVRRunning,
'Game Closed': () => !gameStore.isGameRunning, // Also known as "Outside VRChat"
'Game Running': () => gameStore.isGameRunning, // Also known as "Inside VRChat"
'Desktop Mode': () =>
gameStore.isGameNoVR && gameStore.isGameRunning,
AFK: () =>
notificationsSettingsStore.afkDesktopToast &&
gameStore.isHmdAfk &&
gameStore.isGameRunning &&
!gameStore.isGameNoVR
};
const playNotificationTTS =
notiConditions[notificationsSettingsStore.notificationTTS]?.();
const playDesktopToast =
notiConditions[notificationsSettingsStore.desktopToast]?.() ||
notiConditions['AFK']();
const playOverlayToast =
notiConditions[notificationsSettingsStore.overlayToast]?.();
const playOverlayNotification =
notificationsSettingsStore.overlayNotifications && playOverlayToast;
const playXSNotification =
notificationsSettingsStore.xsNotifications && playOverlayToast;
const playOvrtHudNotifications =
notificationsSettingsStore.ovrtHudNotifications && playOverlayToast;
const playOvrtWristNotifications =
notificationsSettingsStore.ovrtWristNotifications &&
playOverlayToast;
let message = '';
if (noty.title) {
message = `${noty.title}, ${noty.message}`;
} else if (noty.message) {
message = noty.message;
}
const messageList = [
'inviteMessage',
'requestMessage',
'responseMessage'
];
for (let k = 0; k < messageList.length; k++) {
if (
typeof noty.details !== 'undefined' &&
typeof noty.details[messageList[k]] !== 'undefined'
) {
message = `, ${noty.details[messageList[k]]}`;
}
}
if (playNotificationTTS) {
playNotyTTS(noty, displayName, message);
}
if (
playDesktopToast ||
playXSNotification ||
playOvrtHudNotifications ||
playOvrtWristNotifications ||
playOverlayNotification
) {
if (notificationsSettingsStore.imageNotifications) {
notySaveImage(noty).then((image) => {
if (playXSNotification) {
displayXSNotification(noty, message, image);
}
if (
playOvrtHudNotifications ||
playOvrtWristNotifications
) {
displayOvrtNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
noty,
message,
image
);
}
if (playDesktopToast) {
displayDesktopToast(noty, message, image);
}
if (playOverlayNotification) {
displayOverlayNotification(noty, message, image);
}
});
} else {
if (playXSNotification) {
displayXSNotification(noty, message, '');
}
if (playOvrtHudNotifications || playOvrtWristNotifications) {
displayOvrtNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
noty,
message,
''
);
}
if (playDesktopToast) {
displayDesktopToast(noty, message, '');
}
if (playOverlayNotification) {
displayOverlayNotification(noty, message, '');
}
}
}
}
/**
*
* @param {object} noty
* @param {string} displayName
* @param {string} message
*/
async function playNotyTTS(noty, displayName, message) {
if (notificationsSettingsStore.notificationTTSNickName) {
const userId = getUserIdFromNoty(noty);
const memo = await getUserMemo(userId);
if (memo.memo) {
const array = memo.memo.split('\n');
const nickName = array[0];
displayName = nickName;
}
}
switch (noty.type) {
case 'OnPlayerJoined':
notificationsSettingsStore.speak(`${displayName} has joined`);
break;
case 'OnPlayerLeft':
notificationsSettingsStore.speak(`${displayName} has left`);
break;
case 'OnPlayerJoining':
notificationsSettingsStore.speak(`${displayName} is joining`);
break;
case 'GPS':
notificationsSettingsStore.speak(
`${displayName} is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`
);
break;
case 'Online':
let locationName = '';
if (noty.worldName) {
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`;
}
notificationsSettingsStore.speak(
`${displayName} has logged in${locationName}`
);
break;
case 'Offline':
notificationsSettingsStore.speak(
`${displayName} has logged out`
);
break;
case 'Status':
notificationsSettingsStore.speak(
`${displayName} status is now ${noty.status} ${noty.statusDescription}`
);
break;
case 'invite':
notificationsSettingsStore.speak(
`${displayName} has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName,
noty.groupName
)}${message}`
);
break;
case 'requestInvite':
notificationsSettingsStore.speak(
`${displayName} has requested an invite${message}`
);
break;
case 'inviteResponse':
notificationsSettingsStore.speak(
`${displayName} has responded to your invite${message}`
);
break;
case 'requestInviteResponse':
notificationsSettingsStore.speak(
`${displayName} has responded to your invite request${message}`
);
break;
case 'friendRequest':
notificationsSettingsStore.speak(
`${displayName} has sent you a friend request`
);
break;
case 'Friend':
notificationsSettingsStore.speak(
`${displayName} is now your friend`
);
break;
case 'Unfriend':
notificationsSettingsStore.speak(
`${displayName} is no longer your friend`
);
break;
case 'TrustLevel':
notificationsSettingsStore.speak(
`${displayName} trust level is now ${noty.trustLevel}`
);
break;
case 'DisplayName':
notificationsSettingsStore.speak(
`${noty.previousDisplayName} changed their name to ${noty.displayName}`
);
break;
case 'boop':
notificationsSettingsStore.speak(noty.message);
break;
case 'groupChange':
notificationsSettingsStore.speak(
`${displayName} ${noty.message}`
);
break;
case 'group.announcement':
notificationsSettingsStore.speak(noty.message);
break;
case 'group.informative':
notificationsSettingsStore.speak(noty.message);
break;
case 'group.invite':
notificationsSettingsStore.speak(noty.message);
break;
case 'group.joinRequest':
notificationsSettingsStore.speak(noty.message);
break;
case 'group.transfer':
notificationsSettingsStore.speak(noty.message);
break;
case 'group.queueReady':
notificationsSettingsStore.speak(noty.message);
break;
case 'instance.closed':
notificationsSettingsStore.speak(noty.message);
break;
case 'PortalSpawn':
if (displayName) {
notificationsSettingsStore.speak(
`${displayName} has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
)}`
);
} else {
notificationsSettingsStore.speak(
'User has spawned a portal'
);
}
break;
case 'AvatarChange':
notificationsSettingsStore.speak(
`${displayName} changed into avatar ${noty.name}`
);
break;
case 'ChatBoxMessage':
notificationsSettingsStore.speak(
`${displayName} said ${noty.text}`
);
break;
case 'Event':
notificationsSettingsStore.speak(noty.data);
break;
case 'External':
notificationsSettingsStore.speak(noty.message);
break;
case 'VideoPlay':
notificationsSettingsStore.speak(
`Now playing: ${noty.notyName}`
);
break;
case 'BlockedOnPlayerJoined':
notificationsSettingsStore.speak(
`Blocked user ${displayName} has joined`
);
break;
case 'BlockedOnPlayerLeft':
notificationsSettingsStore.speak(
`Blocked user ${displayName} has left`
);
break;
case 'MutedOnPlayerJoined':
notificationsSettingsStore.speak(
`Muted user ${displayName} has joined`
);
break;
case 'MutedOnPlayerLeft':
notificationsSettingsStore.speak(
`Muted user ${displayName} has left`
);
break;
case 'Blocked':
notificationsSettingsStore.speak(
`${displayName} has blocked you`
);
break;
case 'Unblocked':
notificationsSettingsStore.speak(
`${displayName} has unblocked you`
);
break;
case 'Muted':
notificationsSettingsStore.speak(
`${displayName} has muted you`
);
break;
case 'Unmuted':
notificationsSettingsStore.speak(
`${displayName} has unmuted you`
);
break;
}
}
/**
*
* @param {object} noty
* @returns
*/
async function notySaveImage(noty) {
const imageUrl = await notyGetImage(noty);
let fileId = extractFileId(imageUrl);
let fileVersion = extractFileVersion(imageUrl);
let imageLocation = '';
try {
if (fileId && fileVersion) {
imageLocation = await AppApi.GetImage(
imageUrl,
fileId,
fileVersion
);
} else if (imageUrl && imageUrl.startsWith('http')) {
fileVersion = imageUrl.split('/').pop(); // 1416226261.thumbnail-500.png
fileId = fileVersion.split('.').shift(); // 1416226261
imageLocation = await AppApi.GetImage(
imageUrl,
fileId,
fileVersion
);
}
} catch (err) {
console.error(imageUrl, err);
}
return imageLocation;
}
function displayDesktopToast(noty, message, image) {
switch (noty.type) {
case 'OnPlayerJoined':
desktopNotification(noty.displayName, 'has joined', image);
break;
case 'OnPlayerLeft':
desktopNotification(noty.displayName, 'has left', image);
break;
case 'OnPlayerJoining':
desktopNotification(noty.displayName, 'is joining', image);
break;
case 'GPS':
desktopNotification(
noty.displayName,
`is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`,
image
);
break;
case 'Online':
let locationName = '';
if (noty.worldName) {
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`;
}
desktopNotification(
noty.displayName,
`has logged in${locationName}`,
image
);
break;
case 'Offline':
desktopNotification(noty.displayName, 'has logged out', image);
break;
case 'Status':
desktopNotification(
noty.displayName,
`status is now ${noty.status} ${noty.statusDescription}`,
image
);
break;
case 'invite':
desktopNotification(
noty.senderUsername,
`has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
)}${message}`,
image
);
break;
case 'requestInvite':
desktopNotification(
noty.senderUsername,
`has requested an invite${message}`,
image
);
break;
case 'inviteResponse':
desktopNotification(
noty.senderUsername,
`has responded to your invite${message}`,
image
);
break;
case 'requestInviteResponse':
desktopNotification(
noty.senderUsername,
`has responded to your invite request${message}`,
image
);
break;
case 'friendRequest':
desktopNotification(
noty.senderUsername,
'has sent you a friend request',
image
);
break;
case 'Friend':
desktopNotification(
noty.displayName,
'is now your friend',
image
);
break;
case 'Unfriend':
desktopNotification(
noty.displayName,
'is no longer your friend',
image
);
break;
case 'TrustLevel':
desktopNotification(
noty.displayName,
`trust level is now ${noty.trustLevel}`,
image
);
break;
case 'DisplayName':
desktopNotification(
noty.previousDisplayName,
`changed their name to ${noty.displayName}`,
image
);
break;
case 'boop':
desktopNotification(noty.senderUsername, noty.message, image);
break;
case 'groupChange':
desktopNotification(noty.senderUsername, noty.message, image);
break;
case 'group.announcement':
desktopNotification('Group Announcement', noty.message, image);
break;
case 'group.informative':
desktopNotification('Group Informative', noty.message, image);
break;
case 'group.invite':
desktopNotification('Group Invite', noty.message, image);
break;
case 'group.joinRequest':
desktopNotification('Group Join Request', noty.message, image);
break;
case 'group.transfer':
desktopNotification(
'Group Transfer Request',
noty.message,
image
);
break;
case 'group.queueReady':
desktopNotification(
'Instance Queue Ready',
noty.message,
image
);
break;
case 'instance.closed':
desktopNotification('Instance Closed', noty.message, image);
break;
case 'PortalSpawn':
if (noty.displayName) {
desktopNotification(
noty.displayName,
`has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
)}`,
image
);
} else {
desktopNotification('', 'User has spawned a portal', image);
}
break;
case 'AvatarChange':
desktopNotification(
noty.displayName,
`changed into avatar ${noty.name}`,
image
);
break;
case 'ChatBoxMessage':
desktopNotification(
noty.displayName,
`said ${noty.text}`,
image
);
break;
case 'Event':
desktopNotification('Event', noty.data, image);
break;
case 'External':
desktopNotification('External', noty.message, image);
break;
case 'VideoPlay':
desktopNotification('Now playing', noty.notyName, image);
break;
case 'BlockedOnPlayerJoined':
desktopNotification(
noty.displayName,
'blocked user has joined',
image
);
break;
case 'BlockedOnPlayerLeft':
desktopNotification(
noty.displayName,
'blocked user has left',
image
);
break;
case 'MutedOnPlayerJoined':
desktopNotification(
noty.displayName,
'muted user has joined',
image
);
break;
case 'MutedOnPlayerLeft':
desktopNotification(
noty.displayName,
'muted user has left',
image
);
break;
case 'Blocked':
desktopNotification(noty.displayName, 'has blocked you', image);
break;
case 'Unblocked':
desktopNotification(
noty.displayName,
'has unblocked you',
image
);
break;
case 'Muted':
desktopNotification(noty.displayName, 'has muted you', image);
break;
case 'Unmuted':
desktopNotification(noty.displayName, 'has unmuted you', image);
break;
}
}
/**
*
* @param {string} noty
* @param {string} message
* @param {string} imageFile
*/
function displayOverlayNotification(noty, message, imageFile) {
let image = '';
if (imageFile) {
image = `file:///${imageFile}`;
}
AppApi.ExecuteVrOverlayFunction(
'playNoty',
JSON.stringify({ noty, message, image })
);
}
/**
*
* @param {any} noty
* @param {string} message
* @param {string} image
*/
function displayXSNotification(noty, message, image) {
const timeout = Math.floor(
parseInt(
notificationsSettingsStore.notificationTimeout.toString(),
10
) / 1000
);
const opacity =
parseFloat(advancedSettingsStore.notificationOpacity.toString()) /
100;
switch (noty.type) {
case 'OnPlayerJoined':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has joined`,
timeout,
opacity,
image
);
break;
case 'OnPlayerLeft':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has left`,
timeout,
opacity,
image
);
break;
case 'OnPlayerJoining':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} is joining`,
timeout,
opacity,
image
);
break;
case 'GPS':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`,
timeout,
opacity,
image
);
break;
case 'Online':
let locationName = '';
if (noty.worldName) {
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`;
}
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has logged in${locationName}`,
timeout,
opacity,
image
);
break;
case 'Offline':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has logged out`,
timeout,
opacity,
image
);
break;
case 'Status':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`,
timeout,
opacity,
image
);
break;
case 'invite':
AppApi.XSNotification(
'VRCX',
`${
noty.senderUsername
} has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
)}${message}`,
timeout,
opacity,
image
);
break;
case 'requestInvite':
AppApi.XSNotification(
'VRCX',
`${noty.senderUsername} has requested an invite${message}`,
timeout,
opacity,
image
);
break;
case 'inviteResponse':
AppApi.XSNotification(
'VRCX',
`${noty.senderUsername} has responded to your invite${message}`,
timeout,
opacity,
image
);
break;
case 'requestInviteResponse':
AppApi.XSNotification(
'VRCX',
`${noty.senderUsername} has responded to your invite request${message}`,
timeout,
opacity,
image
);
break;
case 'friendRequest':
AppApi.XSNotification(
'VRCX',
`${noty.senderUsername} has sent you a friend request`,
timeout,
opacity,
image
);
break;
case 'Friend':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} is now your friend`,
timeout,
opacity,
image
);
break;
case 'Unfriend':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} is no longer your friend`,
timeout,
opacity,
image
);
break;
case 'TrustLevel':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} trust level is now ${noty.trustLevel}`,
timeout,
opacity,
image
);
break;
case 'DisplayName':
AppApi.XSNotification(
'VRCX',
`${noty.previousDisplayName} changed their name to ${noty.displayName}`,
timeout,
opacity,
image
);
break;
case 'boop':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'groupChange':
AppApi.XSNotification(
'VRCX',
`${noty.senderUsername}: ${noty.message}`,
timeout,
opacity,
image
);
break;
case 'group.announcement':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.informative':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.invite':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.joinRequest':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.transfer':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.queueReady':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'instance.closed':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'PortalSpawn':
if (noty.displayName) {
AppApi.XSNotification(
'VRCX',
`${
noty.displayName
} has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
)}`,
timeout,
opacity,
image
);
} else {
AppApi.XSNotification(
'VRCX',
'User has spawned a portal',
timeout,
opacity,
image
);
}
break;
case 'AvatarChange':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} changed into avatar ${noty.name}`,
timeout,
opacity,
image
);
break;
case 'ChatBoxMessage':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} said ${noty.text}`,
timeout,
opacity,
image
);
break;
case 'Event':
AppApi.XSNotification(
'VRCX',
noty.data,
timeout,
opacity,
image
);
break;
case 'External':
AppApi.XSNotification(
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'VideoPlay':
AppApi.XSNotification(
'VRCX',
`Now playing: ${noty.notyName}`,
timeout,
opacity,
image
);
break;
case 'BlockedOnPlayerJoined':
AppApi.XSNotification(
'VRCX',
`Blocked user ${noty.displayName} has joined`,
timeout,
opacity,
image
);
break;
case 'BlockedOnPlayerLeft':
AppApi.XSNotification(
'VRCX',
`Blocked user ${noty.displayName} has left`,
timeout,
opacity,
image
);
break;
case 'MutedOnPlayerJoined':
AppApi.XSNotification(
'VRCX',
`Muted user ${noty.displayName} has joined`,
timeout,
opacity,
image
);
break;
case 'MutedOnPlayerLeft':
AppApi.XSNotification(
'VRCX',
`Muted user ${noty.displayName} has left`,
timeout,
opacity,
image
);
break;
case 'Blocked':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has blocked you`,
timeout,
opacity,
image
);
break;
case 'Unblocked':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has unblocked you`,
timeout,
opacity,
image
);
break;
case 'Muted':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has muted you`,
timeout,
opacity,
image
);
break;
case 'Unmuted':
AppApi.XSNotification(
'VRCX',
`${noty.displayName} has unmuted you`,
timeout,
opacity,
image
);
break;
}
}
function displayOvrtNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
noty,
message,
image
) {
const timeout = Math.floor(
parseInt(
notificationsSettingsStore.notificationTimeout.toString(),
10
) / 1000
);
const opacity =
parseFloat(advancedSettingsStore.notificationOpacity.toString()) /
100;
switch (noty.type) {
case 'OnPlayerJoined':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has joined`,
timeout,
opacity,
image
);
break;
case 'OnPlayerLeft':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has left`,
timeout,
opacity,
image
);
break;
case 'OnPlayerJoining':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} is joining`,
timeout,
opacity,
image
);
break;
case 'GPS':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} is in ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`,
timeout,
opacity,
image
);
break;
case 'Online':
let locationName = '';
if (noty.worldName) {
locationName = ` to ${displayLocation(
noty.location,
noty.worldName,
noty.groupName
)}`;
}
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has logged in${locationName}`,
timeout,
opacity,
image
);
break;
case 'Offline':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has logged out`,
timeout,
opacity,
image
);
break;
case 'Status':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`,
timeout,
opacity,
image
);
break;
case 'invite':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${
noty.senderUsername
} has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
)}${message}`,
timeout,
opacity,
image
);
break;
case 'requestInvite':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.senderUsername} has requested an invite${message}`,
timeout,
opacity,
image
);
break;
case 'inviteResponse':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.senderUsername} has responded to your invite${message}`,
timeout,
opacity,
image
);
break;
case 'requestInviteResponse':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.senderUsername} has responded to your invite request${message}`,
timeout,
opacity,
image
);
break;
case 'friendRequest':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.senderUsername} has sent you a friend request`,
timeout,
opacity,
image
);
break;
case 'Friend':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} is now your friend`,
timeout,
opacity,
image
);
break;
case 'Unfriend':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} is no longer your friend`,
timeout,
opacity,
image
);
break;
case 'TrustLevel':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} trust level is now ${noty.trustLevel}`,
timeout,
opacity,
image
);
break;
case 'DisplayName':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.previousDisplayName} changed their name to ${noty.displayName}`,
timeout,
opacity,
image
);
break;
case 'boop':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'groupChange':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.senderUsername}: ${noty.message}`,
timeout,
opacity,
image
);
break;
case 'group.announcement':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.informative':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.invite':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.joinRequest':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.transfer':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'group.queueReady':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'instance.closed':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'PortalSpawn':
if (noty.displayName) {
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${
noty.displayName
} has spawned a portal to ${displayLocation(
noty.instanceId,
noty.worldName,
noty.groupName
)}`,
timeout,
opacity,
image
);
} else {
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
'User has spawned a portal',
timeout,
opacity,
image
);
}
break;
case 'AvatarChange':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} changed into avatar ${noty.name}`,
timeout,
opacity,
image
);
break;
case 'ChatBoxMessage':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} said ${noty.text}`,
timeout,
opacity,
image
);
break;
case 'Event':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.data,
timeout,
opacity,
image
);
break;
case 'External':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
noty.message,
timeout,
opacity,
image
);
break;
case 'VideoPlay':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`Now playing: ${noty.notyName}`,
timeout,
opacity,
image
);
break;
case 'BlockedOnPlayerJoined':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`Blocked user ${noty.displayName} has joined`,
timeout,
opacity,
image
);
break;
case 'BlockedOnPlayerLeft':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`Blocked user ${noty.displayName} has left`,
timeout,
opacity,
image
);
break;
case 'MutedOnPlayerJoined':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`Muted user ${noty.displayName} has joined`,
timeout,
opacity,
image
);
break;
case 'MutedOnPlayerLeft':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`Muted user ${noty.displayName} has left`,
timeout,
opacity,
image
);
break;
case 'Blocked':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has blocked you`,
timeout,
opacity,
image
);
break;
case 'Unblocked':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has unblocked you`,
timeout,
opacity,
image
);
break;
case 'Muted':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has muted you`,
timeout,
opacity,
image
);
break;
case 'Unmuted':
AppApi.OVRTNotification(
playOvrtHudNotifications,
playOvrtWristNotifications,
'VRCX',
`${noty.displayName} has unmuted you`,
timeout,
opacity,
image
);
break;
}
}
/**
*
* @param {object} noty
* @returns
*/
function getUserIdFromNoty(noty) {
let userId = '';
if (noty.userId) {
userId = noty.userId;
} else if (noty.senderUserId) {
userId = noty.senderUserId;
} else if (noty.sourceUserId) {
userId = noty.sourceUserId;
} else if (noty.displayName) {
for (const ref of userStore.cachedUsers.values()) {
if (ref.displayName === noty.displayName) {
userId = ref.id;
break;
}
}
}
return userId;
}
/**
*
* @param {object} noty
* @returns
*/
async function notyGetImage(noty) {
let imageUrl = '';
const userId = getUserIdFromNoty(noty);
if (noty.thumbnailImageUrl) {
imageUrl = noty.thumbnailImageUrl;
} else if (noty.details && noty.details.imageUrl) {
imageUrl = noty.details.imageUrl;
} else if (noty.imageUrl) {
imageUrl = noty.imageUrl;
} else if (userId && !userId.startsWith('grp_')) {
imageUrl = await userRequest
.getCachedUser({
userId
})
.catch((err) => {
console.error(err);
return '';
})
.then((args) => {
if (!args.json) {
return '';
}
if (
appearanceSettingsStore.displayVRCPlusIconsAsAvatar &&
args.json.userIcon
) {
return args.json.userIcon;
}
if (args.json.profilePicOverride) {
return args.json.profilePicOverride;
}
return args.json.currentAvatarThumbnailImageUrl;
});
}
return imageUrl;
}
/**
*
* @param {string} displayName
* @param {string} message
* @param {string} image
*/
function desktopNotification(displayName, message, image) {
if (WINDOWS) {
AppApi.DesktopNotification(displayName, message, image);
} else {
window.electron.desktopNotification(displayName, message, image);
}
}
function queueGameLogNoty(gamelog) {
const noty = structuredClone(gamelog);
let bias;
// remove join/leave notifications when switching worlds
if (
noty.type === 'OnPlayerJoined'
// noty.type === 'BlockedOnPlayerJoined' ||
// noty.type === 'MutedOnPlayerJoined'
) {
bias = locationStore.lastLocation.date + 30 * 1000; // 30 secs
if (Date.parse(noty.created_at) <= bias) {
return;
}
}
if (
noty.type === 'OnPlayerLeft' ||
noty.type === 'BlockedOnPlayerLeft' ||
noty.type === 'MutedOnPlayerLeft'
) {
bias = locationStore.lastLocationDestinationTime + 5 * 1000; // 5 secs
if (Date.parse(noty.created_at) <= bias) {
return;
}
}
if (
noty.type === 'Notification' ||
noty.type === 'LocationDestination'
// skip unused entries
) {
return;
}
if (noty.type === 'VideoPlay') {
if (!noty.videoName) {
// skip video without name
return;
}
noty.notyName = noty.videoName;
if (noty.displayName) {
// add requester's name to noty
noty.notyName = `${noty.videoName} (${noty.displayName})`;
}
}
if (
noty.type !== 'VideoPlay' &&
noty.displayName === userStore.currentUser.displayName
) {
// remove current user
return;
}
noty.isFriend = false;
noty.isFavorite = false;
if (noty.userId) {
noty.isFriend = friendStore.friends.has(noty.userId);
noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
} else if (noty.displayName) {
for (const ref of userStore.cachedUsers.values()) {
if (ref.displayName === noty.displayName) {
noty.isFriend = friendStore.friends.has(ref.id);
noty.isFavorite = friendStore.localFavoriteFriends.has(
ref.id
);
break;
}
}
}
const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
if (
notyFilter[noty.type] &&
(notyFilter[noty.type] === 'On' ||
notyFilter[noty.type] === 'Everyone' ||
(notyFilter[noty.type] === 'Friends' && noty.isFriend) ||
(notyFilter[noty.type] === 'VIP' && noty.isFavorite))
) {
playNoty(noty);
}
}
function queueFeedNoty(feed) {
const noty = { ...feed };
if (noty.type === 'Avatar') {
return;
}
// hide private worlds from feed
if (
wristOverlaySettingsStore.hidePrivateFromFeed &&
noty.type === 'GPS' &&
noty.location === 'private'
) {
return;
}
noty.isFriend = friendStore.friends.has(noty.userId);
noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
if (
notyFilter[noty.type] &&
(notyFilter[noty.type] === 'Everyone' ||
(notyFilter[noty.type] === 'Friends' && noty.isFriend) ||
(notyFilter[noty.type] === 'VIP' && noty.isFavorite))
) {
playNoty(noty);
}
}
function queueFriendLogNoty(noty) {
if (noty.type === 'FriendRequest') {
return;
}
noty.isFriend = friendStore.friends.has(noty.userId);
noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
if (
notyFilter[noty.type] &&
(notyFilter[noty.type] === 'On' ||
notyFilter[noty.type] === 'Friends' ||
(notyFilter[noty.type] === 'VIP' && noty.isFavorite))
) {
playNoty(noty);
}
}
function queueModerationNoty(noty) {
noty.isFriend = false;
noty.isFavorite = false;
if (noty.userId) {
noty.isFriend = friendStore.friends.has(noty.userId);
noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
}
const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
if (notyFilter[noty.type] && notyFilter[noty.type] === 'On') {
playNoty(noty);
}
}
async function initNotifications() {
notificationInitStatus.value = false;
let tableData = await database.getNotificationsV2();
let notifications = await database.getNotifications();
tableData = tableData.concat(
notifications.filter((n) => !tableData.some((t) => t.id === n.id))
);
tableData.sort(
(a, b) => Date.parse(b.created_at) - Date.parse(a.created_at)
);
tableData.splice(dbVars.maxTableSize);
notificationTable.value.data = tableData;
refreshNotifications();
}
function testNotification() {
playNoty({
type: 'Event',
created_at: new Date().toJSON(),
data: 'Notification Test'
});
}
function acceptFriendRequestNotification(row) {
modalStore
.confirm({
description: t('confirm.accept_friend_request'),
title: t('confirm.title')
})
.then(({ ok }) => {
if (!ok) return;
notificationRequest.acceptFriendRequestNotification({
notificationId: row.id
});
})
.catch(() => {});
}
async function hideNotification(row) {
if (row.type === 'ignoredFriendRequest') {
await friendRequest.deleteHiddenFriendRequest(
{ notificationId: row.id },
row.senderUserId
);
handleNotificationHide(row.id);
} else {
notificationRequest.hideNotification({
notificationId: row.id
});
}
}
function hideNotificationPrompt(row) {
modalStore
.confirm({
description: t('confirm.decline_type', { type: row.type }),
title: t('confirm.title')
})
.then(({ ok }) => {
if (ok) hideNotification(row);
})
.catch(() => {});
}
function acceptRequestInvite(row) {
modalStore
.confirm({
description: t('confirm.send_invite'),
title: t('confirm.title')
})
.then(({ ok }) => {
if (!ok) return;
let currentLocation = locationStore.lastLocation.location;
if (locationStore.lastLocation.location === 'traveling') {
currentLocation = locationStore.lastLocationDestination;
}
if (!currentLocation) {
currentLocation = userStore.currentUser?.$locationTag;
}
const L = parseLocation(currentLocation);
worldRequest
.getCachedWorld({ worldId: L.worldId })
.then((args) => {
notificationRequest
.sendInvite(
{
instanceId: L.tag,
worldId: L.tag,
worldName: args.ref.name,
rsvp: true
},
row.senderUserId
)
.then((_args) => {
toast(t('message.invite.sent'));
notificationRequest.hideNotification({
notificationId: row.id
});
return _args;
});
});
})
.catch(() => {});
}
function sendNotificationResponse(notificationId, responses, responseType) {
if (!Array.isArray(responses) || responses.length === 0) return;
let responseData = '';
for (let i = 0; i < responses.length; i++) {
if (responses[i].type === responseType) {
responseData = responses[i].data;
break;
}
}
const params = { notificationId, responseType, responseData };
notificationRequest.sendNotificationResponse(params).then((args) => {
console.log('Notification response', args);
if (!args.json) return;
handleNotificationV2Hide(notificationId);
new Noty({
type: 'success',
text: escapeTag(args.json)
}).show();
});
}
function deleteNotificationLog(row) {
const idx = notificationTable.value.data.findIndex(
(e) => e.id === row.id
);
if (idx !== -1) {
notificationTable.value.data.splice(idx, 1);
}
if (
row.type !== 'friendRequest' &&
row.type !== 'ignoredFriendRequest'
) {
if (!row.version || row.version < 2) {
database.deleteNotification(row.id);
} else {
database.deleteNotificationV2(row.id);
}
}
}
function deleteNotificationLogPrompt(row) {
modalStore
.confirm({
description: t('confirm.delete_type', { type: row.type }),
title: t('confirm.title')
})
.then(({ ok }) => {
if (ok) deleteNotificationLog(row);
})
.catch(() => {});
}
function isNotificationExpired(notification) {
if (notification.$isExpired !== undefined) {
return notification.$isExpired;
}
if (!notification.expiresAt) {
return false;
}
const expiresAt = dayjs(notification.expiresAt);
return expiresAt.isValid() && dayjs().isSameOrAfter(expiresAt);
}
function openNotificationLink(link) {
if (!link) {
return;
}
const data = link.split(':');
if (!data.length) {
return;
}
switch (data[0]) {
case 'group':
groupStore.showGroupDialog(data[1]);
break;
case 'user':
userStore.showUserDialog(data[1]);
break;
case 'event':
const ids = data[1].split(',');
if (ids.length < 2) {
console.error('Invalid event notification link:', data[1]);
return;
}
groupStore.showGroupDialog(ids[0]);
// ids[1] cal_ is the event id
break;
case 'openNotificationLink':
default:
toast.error('Unsupported notification link type');
break;
}
}
return {
notificationInitStatus,
notificationTable,
unseenNotifications,
isNotificationsLoading,
initNotifications,
expireNotification,
refreshNotifications,
queueNotificationNoty,
playNoty,
queueGameLogNoty,
queueFeedNoty,
queueFriendLogNoty,
queueModerationNoty,
handleNotificationAccept,
handleNotificationSee,
handlePipelineNotification,
handleNotificationV2Update,
handleNotificationHide,
handleNotificationV2Hide,
handleNotification,
handleNotificationV2,
testNotification,
// Notification actions
acceptFriendRequestNotification,
hideNotification,
hideNotificationPrompt,
acceptRequestInvite,
sendNotificationResponse,
deleteNotificationLog,
deleteNotificationLogPrompt,
isNotificationCenterOpen,
friendNotifications,
groupNotifications,
otherNotifications,
unseenFriendNotifications,
unseenGroupNotifications,
unseenOtherNotifications,
hasUnseenNotifications,
getNotificationCategory,
isNotificationExpired,
openNotificationLink
};
});