Search by userNotes, notes in playerList and friendsList

This commit is contained in:
Natsumi
2025-06-09 09:07:52 +12:00
parent 98c3e43561
commit 5b7da6a91f
8 changed files with 209 additions and 15 deletions

View File

@@ -155,6 +155,24 @@ const userReq = {
window.API.$emit('USER:CURRENT:SAVE', args);
return args;
});
},
/**
* @param params {{ offset: number, n: number }}
* @returns {Promise<{json: any, params}>}
*/
getUserNotes(params) {
return window.API.call(`userNotes`, {
method: 'GET',
params
}).then((json) => {
const args = {
json,
params
};
// window.API.$emit('USER:NOTES', args);
return args;
});
}
};
// #endregion

View File

@@ -154,6 +154,8 @@ import _groups from './classes/groups.js';
import _vrcRegistry from './classes/vrcRegistry.js';
import _restoreFriendOrder from './classes/restoreFriendOrder.js';
import { userNotes } from './classes/userNotes.js';
import pugTemplate from './app.pug';
// API classes
@@ -721,16 +723,16 @@ console.log(`isLinux: ${LINUX}`);
API.applyUser = function (json) {
var ref = this.cachedUsers.get(json.id);
if (typeof json.statusDescription !== 'undefined') {
if (json.statusDescription) {
json.statusDescription = $utils.replaceBioSymbols(
json.statusDescription
);
json.statusDescription = $app.removeEmojis(json.statusDescription);
}
if (typeof json.bio !== 'undefined') {
if (json.bio) {
json.bio = $utils.replaceBioSymbols(json.bio);
}
if (typeof json.note !== 'undefined') {
if (json.note) {
json.note = $utils.replaceBioSymbols(json.note);
}
if (json.currentAvatarImageUrl === $app.robotUrl) {
@@ -762,7 +764,7 @@ console.log(`isLinux: ${LINUX}`);
last_platform: '',
location: '',
platform: '',
note: '',
note: null, // keep as null, to detect deleted notes
profilePicOverride: '',
profilePicOverrideThumbnail: '',
pronouns: '',
@@ -907,6 +909,9 @@ console.log(`isLinux: ${LINUX}`);
props[prop] = [tobe, asis];
}
}
if ($ref.note !== null && $ref.note !== ref.note) {
userNotes.checkNote(ref.id, ref.note);
}
// FIXME
// if the status is offline, just ignore status and statusDescription only.
if (has && ref.status !== 'offline' && $ref.status !== 'offline') {
@@ -4238,6 +4243,7 @@ console.log(`isLinux: ${LINUX}`);
}
await $app.getAvatarHistory();
await $app.getAllUserMemos();
userNotes.init();
if ($app.randomUserColours) {
$app.getNameColour(this.currentUser.id).then((colour) => {
this.currentUser.$userColour = colour;
@@ -6271,7 +6277,7 @@ console.log(`isLinux: ${LINUX}`);
);
$app.data.hideUserMemos = await configRepository.getBool(
'VRCX_hideUserMemos',
false
true
);
$app.data.hideUnfriends = await configRepository.getBool(
'VRCX_hideUnfriends',

118
src/classes/userNotes.js Normal file
View File

@@ -0,0 +1,118 @@
import { userRequest } from '../api';
import database from '../service/database.js';
import utils from '../classes/utils';
import * as workerTimers from 'worker-timers';
const userNotes = {
lastNoteCheck: null,
lastDbNoteDate: null,
notes: new Map(),
async init() {
try {
this.lastNoteCheck = new Date();
this.notes.clear();
// todo: get users from store
const users = window.API.cachedUsers;
const dbNotes = await database.getAllUserNotes();
for (const note of dbNotes) {
this.notes.set(note.userId, note.note);
const user = users.get(note.userId);
if (user) {
user.note = note.note;
}
if (
!this.lastDbNoteDate ||
this.lastDbNoteDate < note.createdAt
) {
this.lastDbNoteDate = note.createdAt;
}
}
await this.getLatestUserNotes();
} catch (error) {
console.error('Error initializing user notes:', error);
}
},
async getLatestUserNotes() {
this.lastNoteCheck = new Date();
const params = {
offset: 0,
n: 10 // start light
};
const newNotes = new Map();
let done = false;
try {
for (let i = 0; i < 100; i++) {
params.offset = i * params.n;
const args = await userRequest.getUserNotes(params);
for (const note of args.json) {
if (
this.lastDbNoteDate &&
this.lastDbNoteDate > note.createdAt
) {
done = true;
}
if (
!this.lastDbNoteDate ||
this.lastDbNoteDate < note.createdAt
) {
this.lastDbNoteDate = note.createdAt;
}
note.note = utils.replaceBioSymbols(note.note);
newNotes.set(note.targetUserId, note);
}
if (done || args.json.length === 0) {
break;
}
params.n = 100; // crank it after first run
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 1000);
});
}
} catch (error) {
console.error('Error fetching user notes:', error);
}
// todo: get users from store
const users = window.API.cachedUsers;
for (const note of newNotes.values()) {
const newNote = {
userId: note.targetUserId,
displayName: note.targetUser?.displayName || note.targetUserId,
note: note.note,
createdAt: note.createdAt
};
await database.addUserNote(newNote);
this.notes.set(note.targetUserId, note.note);
const user = users.get(note.targetUserId);
if (user) {
user.note = note.note;
}
}
},
async checkNote(userId, newNote) {
console.log('checkNote', userId, newNote);
// last check was more than than 5 minutes ago
if (
!this.lastNoteCheck ||
this.lastNoteCheck.getTime() + 5 * 60 * 1000 > Date.now()
) {
return;
}
const existingNote = this.notes.get(userId);
if (typeof existingNote !== 'undefined' && !newNote) {
console.log('deleting note', userId);
this.notes.delete(userId);
await database.deleteUserNote(userId);
return;
}
if (typeof existingNote === 'undefined' || existingNote !== newNote) {
console.log('detected note change', userId, newNote);
await this.getLatestUserNotes();
}
}
};
export { userNotes };

View File

@@ -3005,7 +3005,10 @@
}
const ref = API.cachedUsers.get(targetUserId);
if (typeof ref !== 'undefined') {
ref.note = _note;
API.applyUser({
id: targetUserId,
note: _note
});
}
}

View File

@@ -1917,6 +1917,7 @@
"rank": "Rank",
"language": "Language",
"bioLink": "Bio Links",
"note": "Note",
"date": "Date",
"user": "User",
"type": "Type",

View File

@@ -43,13 +43,7 @@ class Database {
`CREATE TABLE IF NOT EXISTS ${Database.userPrefix}_avatar_history (avatar_id TEXT PRIMARY KEY, created_at TEXT, time INTEGER)`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS memos (user_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS world_memos (world_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS avatar_memos (avatar_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
`CREATE TABLE IF NOT EXISTS ${Database.userPrefix}_notes (user_id TEXT PRIMARY KEY, display_name TEXT, note TEXT, created_at TEXT)`
);
}
@@ -87,6 +81,15 @@ class Database {
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS favorite_avatar (id INTEGER PRIMARY KEY, created_at TEXT, avatar_id TEXT, group_name TEXT)`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS memos (user_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS world_memos (world_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS avatar_memos (avatar_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
);
}
async getFeedDatabase() {
@@ -2846,6 +2849,43 @@ class Database {
);
return result;
}
// user notes
async addUserNote(note) {
sqliteService.executeNonQuery(
`INSERT OR REPLACE INTO ${Database.userPrefix}_notes (user_id, display_name, note, created_at) VALUES (@user_id, @display_name, @note, @created_at)`,
{
'@user_id': note.userId,
'@display_name': note.displayName,
'@note': note.note,
'@created_at': note.createdAt
}
);
}
async getAllUserNotes() {
var data = [];
await sqliteService.execute((dbRow) => {
var row = {
userId: dbRow[0],
displayName: dbRow[1],
note: dbRow[2],
createdAt: dbRow[3]
};
data.push(row);
}, `SELECT user_id, display_name, note, created_at FROM ${Database.userPrefix}_notes`);
return data;
}
async deleteUserNote(userId) {
sqliteService.executeNonQuery(
`DELETE FROM ${Database.userPrefix}_notes WHERE user_id = @userId`,
{
'@userId': userId
}
);
}
}
var self = new Database();

View File

@@ -76,7 +76,7 @@
:placeholder="$t('view.friend_list.filter_placeholder')"
@change="friendsListSearchChange">
<el-option
v-for="type in ['Display Name', 'User Name', 'Rank', 'Status', 'Bio', 'Memo']"
v-for="type in ['Display Name', 'User Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo']"
:key="type"
:label="type"
:value="type"></el-option>
@@ -347,7 +347,7 @@
this.friendsListTable.data = [];
let filters = [...this.friendsListSearchFilters];
if (filters.length === 0) {
filters = ['Display Name', 'Rank', 'Status', 'Bio', 'Memo'];
filters = ['Display Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo'];
}
const results = [];
if (this.friendsListSearch) {
@@ -379,6 +379,9 @@
if (!match && filters.includes('Memo') && ctx.memo) {
match = utils.localeIncludes(ctx.memo, query, this.stringComparer);
}
if (!match && filters.includes('Note') && ctx.ref.note) {
match = utils.localeIncludes(ctx.ref.note, query, this.stringComparer);
}
if (!match && filters.includes('Bio') && ctx.ref.bio) {
match = utils.localeIncludes(ctx.ref.bio, query, this.stringComparer);
}

View File

@@ -814,6 +814,11 @@
</div>
</template>
</el-table-column>
<el-table-column :label="t('table.playerList.note')" width="150" prop="ref.note">
<template #default="scope">
<span v-text="scope.row.ref.note"></span>
</template>
</el-table-column>
</data-tables>
</div>
</div>