diff --git a/package-lock.json b/package-lock.json
index c1b6a9e6..f8b8ecb8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,7 @@
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"@vitejs/plugin-vue": "^6.0.3",
+ "@vitejs/plugin-vue-jsx": "^5.1.3",
"@vueuse/core": "^14.1.0",
"animate.css": "^4.1.1",
"babel-runtime": "^6.26.0",
@@ -151,6 +152,19 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
@@ -178,6 +192,38 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz",
+ "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.28.5",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
@@ -188,6 +234,20 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+ "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
@@ -220,6 +280,19 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
@@ -230,6 +303,38 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -543,6 +648,26 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz",
+ "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@@ -6156,6 +6281,87 @@
"vue": "^3.2.25"
}
},
+ "node_modules/@vitejs/plugin-vue-jsx": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-5.1.3.tgz",
+ "integrity": "sha512-I6Zr8cYVr5WHMW5gNOP09DNqW9rgO8RX73Wa6Czgq/0ndpTfJM4vfDChfOT1+3KtdrNqilNBtNlFwVeB02ZzGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-syntax-typescript": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.28.5",
+ "@rolldown/pluginutils": "^1.0.0-beta.56",
+ "@vue/babel-plugin-jsx": "^2.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0",
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue-jsx/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.58",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.58.tgz",
+ "integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/babel-helper-vue-transform-on": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-2.0.1.tgz",
+ "integrity": "sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/babel-plugin-jsx": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-2.0.1.tgz",
+ "integrity": "sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.4",
+ "@babel/types": "^7.28.4",
+ "@vue/babel-helper-vue-transform-on": "2.0.1",
+ "@vue/babel-plugin-resolve-type": "2.0.1",
+ "@vue/shared": "^3.5.22"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/babel-plugin-resolve-type": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-2.0.1.tgz",
+ "integrity": "sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/parser": "^7.28.4",
+ "@vue/compiler-sfc": "^3.5.22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@vue/compiler-core": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
diff --git a/package.json b/package.json
index a775cbf2..9ad99b79 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"@vitejs/plugin-vue": "^6.0.3",
+ "@vitejs/plugin-vue-jsx": "^5.1.3",
"@vueuse/core": "^14.1.0",
"animate.css": "^4.1.1",
"babel-runtime": "^6.26.0",
diff --git a/src/views/Feed/columns.js b/src/views/Feed/columns.js
deleted file mode 100644
index 7fc4a7a2..00000000
--- a/src/views/Feed/columns.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { ElTag, ElTooltip } from 'element-plus';
-import { h, resolveComponent } from 'vue';
-
-import { formatDateFilter, statusClass } from '../../shared/utils';
-import { i18n } from '../../plugin';
-import { useUserStore } from '../../stores';
-
-const { t } = i18n.global;
-
-export const columns = [
- {
- accessorKey: 'created_at',
- header: () => t('table.feed.date'),
- cell: ({ row }) => {
- const createdAt = row.getValue('created_at');
- return h(
- ElTooltip,
- { placement: 'right' },
- {
- content: () =>
- h('span', formatDateFilter(createdAt, 'long')),
- default: () =>
- h('span', formatDateFilter(createdAt, 'short'))
- }
- );
- }
- },
- {
- accessorKey: 'type',
- header: () => t('table.feed.type'),
- cell: ({ row }) => {
- const type = row.getValue('type');
- return h(
- ElTag,
- { type: 'info', effect: 'plain', size: 'small' },
- () => t(`view.feed.filters.${type}`)
- );
- }
- },
- {
- accessorKey: 'displayName',
- header: () => t('table.feed.user'),
- cell: ({ row }) => {
- const { showUserDialog } = useUserStore();
- const original = row.original;
- return h(
- 'span',
- {
- class: 'x-link table-user',
- style: 'padding-right: 10px',
- onClick: () => showUserDialog(original.userId)
- },
- original.displayName
- );
- }
- },
- {
- id: 'detail',
- header: () => t('table.feed.detail'),
- cell: ({ row }) => {
- const original = row.original;
- const type = original.type;
- const Location = resolveComponent('Location');
- const AvatarInfo = resolveComponent('AvatarInfo');
-
- if (type === 'GPS') {
- return original.location
- ? h(Location, {
- location: original.location,
- hint: original.worldName,
- grouphint: original.groupName
- })
- : null;
- }
-
- if (type === 'Offline' || type === 'Online') {
- return original.location
- ? h(Location, {
- location: original.location,
- hint: original.worldName,
- grouphint: original.groupName
- })
- : null;
- }
-
- if (type === 'Status') {
- if (
- original.statusDescription ===
- original.previousStatusDescription
- ) {
- return h('span', [
- h('i', {
- class: [
- 'x-user-status',
- statusClass(original.previousStatus)
- ]
- }),
- h('span', { class: 'mx-2' }, ' → '),
- h('i', {
- class: [
- 'x-user-status',
- statusClass(original.status)
- ]
- })
- ]);
- }
-
- return h('span', [
- h('i', {
- class: [
- 'x-user-status',
- 'mr-2',
- statusClass(original.status)
- ]
- }),
- h('span', original.statusDescription)
- ]);
- }
-
- if (type === 'Avatar') {
- return h(AvatarInfo, {
- imageurl: original.currentAvatarImageUrl,
- userid: original.userId,
- hintownerid: original.ownerId,
- hintavatarname: original.avatarName,
- avatartags: original.currentAvatarTags
- });
- }
-
- if (type === 'Bio') {
- return h('span', original.bio);
- }
-
- return null;
- }
- }
-];
diff --git a/src/views/Feed/columns.jsx b/src/views/Feed/columns.jsx
new file mode 100644
index 00000000..bd291ac8
--- /dev/null
+++ b/src/views/Feed/columns.jsx
@@ -0,0 +1,154 @@
+import { ElTag } from 'element-plus';
+import { resolveComponent } from 'vue';
+
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger
+} from '../../components/ui/tooltip';
+import { formatDateFilter, statusClass } from '../../shared/utils';
+import { i18n } from '../../plugin';
+import { useUserStore } from '../../stores';
+
+const { t } = i18n.global;
+
+export const columns = [
+ {
+ accessorKey: 'created_at',
+ header: () => t('table.feed.date'),
+ cell: ({ row }) => {
+ const createdAt = row.getValue('created_at');
+ const shortText = formatDateFilter(createdAt, 'short');
+ const longText = formatDateFilter(createdAt, 'long');
+
+ return (
+
+
+
+ {shortText}
+
+
+ {longText}
+
+
+
+ );
+ }
+ },
+ {
+ accessorKey: 'type',
+ header: () => t('table.feed.type'),
+ cell: ({ row }) => {
+ const type = row.getValue('type');
+ return (
+
+ {t(`view.feed.filters.${type}`)}
+
+ );
+ }
+ },
+ {
+ accessorKey: 'displayName',
+ header: () => t('table.feed.user'),
+ cell: ({ row }) => {
+ const { showUserDialog } = useUserStore();
+ const original = row.original;
+ return (
+ showUserDialog(original.userId)}
+ >
+ {original.displayName}
+
+ );
+ }
+ },
+ {
+ id: 'detail',
+ header: () => t('table.feed.detail'),
+ cell: ({ row }) => {
+ const original = row.original;
+ const type = original.type;
+ const Location = resolveComponent('Location');
+ const AvatarInfo = resolveComponent('AvatarInfo');
+
+ if (type === 'GPS') {
+ return original.location ? (
+
+ ) : null;
+ }
+
+ if (type === 'Offline' || type === 'Online') {
+ return original.location ? (
+
+ ) : null;
+ }
+
+ if (type === 'Status') {
+ if (
+ original.statusDescription ===
+ original.previousStatusDescription
+ ) {
+ return (
+
+
+ Ўъ
+
+
+ );
+ }
+
+ return (
+
+
+ {original.statusDescription}
+
+ );
+ }
+
+ if (type === 'Avatar') {
+ return (
+
+ );
+ }
+
+ if (type === 'Bio') {
+ return {original.bio};
+ }
+
+ return null;
+ }
+ }
+];
diff --git a/src/vite.config.js b/src/vite.config.js
index 99609f9d..1237a936 100644
--- a/src/vite.config.js
+++ b/src/vite.config.js
@@ -6,6 +6,7 @@ import { defineConfig, loadEnv } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
import { languageCodes } from './localization/locales';
@@ -66,6 +67,9 @@ export default defineConfig(({ mode }) => {
base: '',
plugins: [
vue(),
+ vueJsx({
+ tsTransform: 'built-in'
+ }),
tailwindcss(),
buildAndUploadSourceMaps &&
import('@sentry/vite-plugin').then(({ sentryVitePlugin }) =>