diff --git a/src/api/user.js b/src/api/user.js index 769c15dc..578289fb 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -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 diff --git a/src/app.js b/src/app.js index 7e1e473d..3b60577d 100644 --- a/src/app.js +++ b/src/app.js @@ -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', diff --git a/src/classes/userNotes.js b/src/classes/userNotes.js new file mode 100644 index 00000000..2bfbe287 --- /dev/null +++ b/src/classes/userNotes.js @@ -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 }; diff --git a/src/components/dialogs/UserDialog/UserDialog.vue b/src/components/dialogs/UserDialog/UserDialog.vue index c45c7ff5..3fb08cb2 100644 --- a/src/components/dialogs/UserDialog/UserDialog.vue +++ b/src/components/dialogs/UserDialog/UserDialog.vue @@ -3005,7 +3005,10 @@ } const ref = API.cachedUsers.get(targetUserId); if (typeof ref !== 'undefined') { - ref.note = _note; + API.applyUser({ + id: targetUserId, + note: _note + }); } } diff --git a/src/localization/en/en.json b/src/localization/en/en.json index 342e57bb..d85b1207 100644 --- a/src/localization/en/en.json +++ b/src/localization/en/en.json @@ -1917,6 +1917,7 @@ "rank": "Rank", "language": "Language", "bioLink": "Bio Links", + "note": "Note", "date": "Date", "user": "User", "type": "Type", diff --git a/src/service/database.js b/src/service/database.js index 5a4325af..febde73f 100644 --- a/src/service/database.js +++ b/src/service/database.js @@ -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(); diff --git a/src/views/FriendList/FriendList.vue b/src/views/FriendList/FriendList.vue index bcdcafcb..92e08986 100644 --- a/src/views/FriendList/FriendList.vue +++ b/src/views/FriendList/FriendList.vue @@ -76,7 +76,7 @@ :placeholder="$t('view.friend_list.filter_placeholder')" @change="friendsListSearchChange"> @@ -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); } diff --git a/src/views/PlayerList/PlayerList.vue b/src/views/PlayerList/PlayerList.vue index a2e4cdf8..1c9e96ef 100644 --- a/src/views/PlayerList/PlayerList.vue +++ b/src/views/PlayerList/PlayerList.vue @@ -814,6 +814,11 @@ + + +