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 @@
+
+
+
+
+