From d5ded750788ab8a1eb5671ba96c56a1df6a6e9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 26 May 2025 23:49:00 +0800 Subject: [PATCH] feat(backend): resize images with imagor Open questions: * Do we have external URLs for images? E.g. we have them for seeds. But in production? * Do we want to apply image transformations on these as well? My current implementation does not apply image transformations as of now. If we want to do that, we will also expose internal URLs in the kubernetes Cluster to the S3 endpoint to the client. TODOs: * The chat component is using a fixed size for all avatars at the moment. Maybe we can pair-program on this how to implement responsive images in this component library. --- .github/workflows/test-backend.yml | 6 +- backend/src/config/index.ts | 4 ++ backend/src/config/test-mock.ts | 52 ++++++++++++++ backend/src/graphql/resolvers/images.ts | 71 +++++++++++++++++++ .../graphql/resolvers/images/imagesS3.spec.ts | 1 + .../src/graphql/resolvers/images/imagesS3.ts | 9 +-- backend/src/graphql/types/type/Image.gql | 1 + .../templates/backend/stateful-set.yaml | 2 + .../templates/imagor/deployment.yaml | 28 ++++++++ .../templates/imagor/secret.yaml | 7 ++ .../templates/imagor/service.yaml | 11 +++ .../ocelot-social/templates/ingress.yaml | 32 ++++++++- .../helm/charts/ocelot-social/values.yaml | 6 ++ deployment/helm/helmfile/secrets/ocelot.yaml | 24 +++++-- docker-compose.override.yml | 20 +++++- webapp/components/BadgeSelection.vue | 2 +- webapp/components/Badges.vue | 2 +- webapp/components/Chat/Chat.vue | 6 +- .../ContributionForm/ContributionForm.vue | 2 +- webapp/components/PostTeaser/PostTeaser.vue | 4 +- .../ResponsiveImage/ResponsiveImage.vue | 24 +++++++ .../features/Admin/Badges/BadgesSection.vue | 2 +- .../generic/ProfileAvatar/ProfileAvatar.vue | 10 ++- webapp/graphql/Fragments.js | 6 ++ webapp/pages/post/_id/_slug/index.vue | 10 ++- webapp/pages/post/edit/_id.vue | 2 +- webapp/plugins/vue-filters.js | 6 -- 27 files changed, 312 insertions(+), 38 deletions(-) create mode 100644 backend/src/config/test-mock.ts create mode 100644 deployment/helm/charts/ocelot-social/templates/imagor/deployment.yaml create mode 100644 deployment/helm/charts/ocelot-social/templates/imagor/secret.yaml create mode 100644 deployment/helm/charts/ocelot-social/templates/imagor/service.yaml create mode 100644 webapp/components/ResponsiveImage/ResponsiveImage.vue diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index a7eff0ef2..f528a8262 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -34,7 +34,7 @@ jobs: run: | docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/ docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar - + - name: Cache docker images id: cache-neo4j uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 @@ -55,7 +55,7 @@ jobs: run: | docker build --target test -t "ocelotsocialnetwork/backend:test" backend/ docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar - + - name: Cache docker images id: cache-backend uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 @@ -80,7 +80,7 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true' needs: [files-changed, build_test_neo4j, build_test_backend] runs-on: ubuntu-latest - permissions: + permissions: checks: write steps: - name: Checkout code diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index d517b3e23..14a0b821b 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -132,8 +132,11 @@ export interface S3Configured { AWS_REGION: string AWS_BUCKET: string S3_PUBLIC_GATEWAY: string | undefined + IMAGOR_SECRET: string | undefined } +export const IMAGOR_SECRET: string | undefined = env.IMAGOR_SECRET + export const isS3configured = (config: typeof s3): config is S3Configured => { return !!( config.AWS_ACCESS_KEY_ID && @@ -176,6 +179,7 @@ const CONFIG = { ...s3, ...options, ...language, + IMAGOR_SECRET, } export type Config = typeof CONFIG diff --git a/backend/src/config/test-mock.ts b/backend/src/config/test-mock.ts new file mode 100644 index 000000000..e9bc4ece7 --- /dev/null +++ b/backend/src/config/test-mock.ts @@ -0,0 +1,52 @@ +import type CONFIG from '.' + +export const TEST_CONFIG: typeof CONFIG = { + NODE_ENV: 'test', + DEBUG: undefined, + TEST: true, + PRODUCTION: false, + PRODUCTION_DB_CLEAN_ALLOW: false, + DISABLED_MIDDLEWARES: [], + SEND_MAIL: false, + + CLIENT_URI: 'http://localhost:3000', + GRAPHQL_URI: 'http://localhost:4000', + JWT_EXPIRES: '2y', + + MAPBOX_TOKEN: undefined, + JWT_SECRET: undefined, + PRIVATE_KEY_PASSPHRASE: undefined, + + NEO4J_URI: 'bolt://localhost:7687', + NEO4J_USERNAME: 'neo4j', + NEO4J_PASSWORD: 'neo4j', + + SENTRY_DSN_BACKEND: undefined, + COMMIT: undefined, + + REDIS_DOMAIN: undefined, + REDIS_PORT: undefined, + REDIS_PASSWORD: undefined, + + AWS_ACCESS_KEY_ID: '', + AWS_SECRET_ACCESS_KEY: '', + AWS_ENDPOINT: '', + AWS_REGION: '', + AWS_BUCKET: '', + S3_PUBLIC_GATEWAY: undefined, + IMAGOR_SECRET: undefined, + + EMAIL_DEFAULT_SENDER: '', + SUPPORT_EMAIL: '', + SUPPORT_URL: '', + APPLICATION_NAME: '', + ORGANIZATION_URL: '', + PUBLIC_REGISTRATION: false, + INVITE_REGISTRATION: true, + INVITE_CODES_PERSONAL_PER_USER: 7, + INVITE_CODES_GROUP_PER_USER: 7, + CATEGORIES_ACTIVE: false, + MAX_PINNED_POSTS: 1, + + LANGUAGE_DEFAULT: 'en', +} diff --git a/backend/src/graphql/resolvers/images.ts b/backend/src/graphql/resolvers/images.ts index ea596a183..46cfbc55a 100644 --- a/backend/src/graphql/resolvers/images.ts +++ b/backend/src/graphql/resolvers/images.ts @@ -1,9 +1,80 @@ +import crypto from 'node:crypto' + +import type { Context } from '@src/server' + import Resolver from './helpers/Resolver' +type UrlResolver = ( + parent: { url: string }, + args: { width?: number; height?: number }, + { + config: { S3_PUBLIC_GATEWAY }, + }: Context, +) => string + +const changeDomain: (opts: { transformations: UrlResolver[] }) => UrlResolver = + ({ transformations }) => + (parent, _args, context) => { + const { config } = context + const { S3_PUBLIC_GATEWAY, AWS_ENDPOINT } = config + if (!(S3_PUBLIC_GATEWAY && AWS_ENDPOINT)) { + return parent.url + } + if (new URL(parent.url).host !== new URL(AWS_ENDPOINT).host) { + // In this case it's an external upload - maybe seeded? + // Let's not change the URL in this case + return parent.url + } + + const publicUrl = new URL(S3_PUBLIC_GATEWAY) + publicUrl.pathname = new URL(parent.url).pathname + const url = publicUrl.href + return chain(...transformations)({ url }, _args, context) + } + +const sign: UrlResolver = ({ url }, _args, { config: { IMAGOR_SECRET } }) => { + if (!IMAGOR_SECRET) { + throw new Error('IMAGOR_SECRET is not set') + } + const newUrl = new URL(url) + const path = newUrl.pathname.replace('/', '') + const hash = crypto + .createHmac('sha1', IMAGOR_SECRET) + .update(path) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + newUrl.pathname = hash + newUrl.pathname + return newUrl.href +} + +const FALLBACK_MAXIMUM_LENGTH = 5000 +const resize: UrlResolver = ({ url }, { height, width }) => { + if (!(height || width)) { + return url + } + const window = `/fit-in/${width ?? FALLBACK_MAXIMUM_LENGTH}x${height ?? FALLBACK_MAXIMUM_LENGTH}` + const newUrl = new URL(url) + newUrl.pathname = window + newUrl.pathname + return newUrl.href +} + +const chain: (...methods: UrlResolver[]) => UrlResolver = (...methods) => { + return (parent, args, context) => { + let { url } = parent + for (const method of methods) { + url = method({ url }, args, context) + } + return url + } +} + export default { Image: { ...Resolver('Image', { undefinedToNull: ['sensitive', 'alt', 'aspectRatio', 'type'], }), + url: changeDomain({ transformations: [sign] }), + transform: changeDomain({ transformations: [resize, sign] }), }, } diff --git a/backend/src/graphql/resolvers/images/imagesS3.spec.ts b/backend/src/graphql/resolvers/images/imagesS3.spec.ts index 2bedec3cd..37c393bb1 100644 --- a/backend/src/graphql/resolvers/images/imagesS3.spec.ts +++ b/backend/src/graphql/resolvers/images/imagesS3.spec.ts @@ -30,6 +30,7 @@ const config: S3Configured = { AWS_ENDPOINT: 'AWS_ENDPOINT', AWS_REGION: 'AWS_REGION', S3_PUBLIC_GATEWAY: undefined, + IMAGOR_SECRET: undefined, } beforeAll(async () => { diff --git a/backend/src/graphql/resolvers/images/imagesS3.ts b/backend/src/graphql/resolvers/images/imagesS3.ts index 66c4a0a69..d51feea27 100644 --- a/backend/src/graphql/resolvers/images/imagesS3.ts +++ b/backend/src/graphql/resolvers/images/imagesS3.ts @@ -16,7 +16,7 @@ import type { FileUpload } from 'graphql-upload' export const images = (config: S3Configured) => { // const widths = [34, 160, 320, 640, 1024] - const { AWS_BUCKET: Bucket, S3_PUBLIC_GATEWAY } = config + const { AWS_BUCKET: Bucket } = config const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = config const s3 = new S3Client({ @@ -105,12 +105,7 @@ export const images = (config: S3Configured) => { const { name, ext } = path.parse(upload.filename) const uniqueFilename = `${uuid()}-${slug(name)}${ext}` const Location = await uploadCallback({ ...upload, uniqueFilename }) - if (!S3_PUBLIC_GATEWAY) { - return Location - } - const publicLocation = new URL(S3_PUBLIC_GATEWAY) - publicLocation.pathname = new URL(Location).pathname - return publicLocation.href + return Location } const s3Upload: FileUploadCallback = async ({ createReadStream, uniqueFilename, mimetype }) => { diff --git a/backend/src/graphql/types/type/Image.gql b/backend/src/graphql/types/type/Image.gql index f171a4b77..1cbe9c78c 100644 --- a/backend/src/graphql/types/type/Image.gql +++ b/backend/src/graphql/types/type/Image.gql @@ -1,5 +1,6 @@ type Image { url: ID!, + transform(width: Int, height: Int): String # urlW34: String, # urlW160: String, # urlW320: String, diff --git a/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml b/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml index 604b79826..b732c695f 100644 --- a/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml +++ b/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml @@ -38,6 +38,8 @@ spec: value: "http://{{ .Release.Name }}-backend:4000" - name: CLIENT_URI value: "https://{{ .Values.domain }}" + - name: S3_PUBLIC_GATEWAY + value: "https://{{ .Values.domain }}/imagor" envFrom: - configMapRef: name: {{ .Release.Name }}-backend-env diff --git a/deployment/helm/charts/ocelot-social/templates/imagor/deployment.yaml b/deployment/helm/charts/ocelot-social/templates/imagor/deployment.yaml new file mode 100644 index 000000000..80bac5e8b --- /dev/null +++ b/deployment/helm/charts/ocelot-social/templates/imagor/deployment.yaml @@ -0,0 +1,28 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{ .Release.Name }}-imagor +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Release.Name }}-imagor + template: + metadata: + labels: + app: {{ .Release.Name }}-imagor + spec: + restartPolicy: Always + containers: + - name: {{ .Release.Name }}-imagor + image: "{{ .Values.imagor.image.repository }}:{{ .Values.imagor.image.tag | default (include "defaultTag" .) }}" + imagePullPolicy: {{ quote .Values.global.image.pullPolicy }} + {{- include "resources" .Values.imagor.resources | indent 8 }} + ports: + - containerPort: 8000 + env: + - name: S3_FORCE_PATH_STYLE + value: "1" + envFrom: + - secretRef: + name: {{ .Release.Name }}-imagor-secret-env diff --git a/deployment/helm/charts/ocelot-social/templates/imagor/secret.yaml b/deployment/helm/charts/ocelot-social/templates/imagor/secret.yaml new file mode 100644 index 000000000..7e11e933f --- /dev/null +++ b/deployment/helm/charts/ocelot-social/templates/imagor/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-imagor-secret-env +type: Opaque +stringData: +{{ .Values.secrets.imagor.env | toYaml | indent 2 }} diff --git a/deployment/helm/charts/ocelot-social/templates/imagor/service.yaml b/deployment/helm/charts/ocelot-social/templates/imagor/service.yaml new file mode 100644 index 000000000..229a039b4 --- /dev/null +++ b/deployment/helm/charts/ocelot-social/templates/imagor/service.yaml @@ -0,0 +1,11 @@ +kind: Service +apiVersion: v1 +metadata: + name: {{ .Release.Name }}-imagor +spec: + ports: + - name: {{ .Release.Name }}-http + port: 8000 + targetPort: 8000 + selector: + app: {{ .Release.Name }}-imagor diff --git a/deployment/helm/charts/ocelot-social/templates/ingress.yaml b/deployment/helm/charts/ocelot-social/templates/ingress.yaml index 56142f650..bf5e202df 100644 --- a/deployment/helm/charts/ocelot-social/templates/ingress.yaml +++ b/deployment/helm/charts/ocelot-social/templates/ingress.yaml @@ -62,4 +62,34 @@ spec: regex: ^https://{{ . }}(.*) replacement: https://{{ $.Values.domain }}${1} permanent: true -{{- end }} \ No newline at end of file +{{- end }} + +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ .Release.Name }}-stripprefix +spec: + stripPrefix: + prefixes: + - /imagor + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-path-prefixes + annotations: + traefik.ingress.kubernetes.io/router.middlewares: "{{ .Release.Namespace }}-{{ .Release.Name }}-stripprefix@kubernetescrd" +spec: + rules: + - host: {{ quote .Values.domain }} + http: + paths: + - path: /imagor + pathType: Prefix + backend: + service: + name: {{ .Release.Name }}-imagor + port: + number: 8000 diff --git a/deployment/helm/charts/ocelot-social/values.yaml b/deployment/helm/charts/ocelot-social/values.yaml index 2213c5007..64867edb6 100644 --- a/deployment/helm/charts/ocelot-social/values.yaml +++ b/deployment/helm/charts/ocelot-social/values.yaml @@ -25,3 +25,9 @@ webapp: maintenance: image: repository: ghcr.io/ocelot-social-community/ocelot-social/maintenance + +imagor: + image: + repository: shumc/imagor + tag: 1.5.4 + diff --git a/deployment/helm/helmfile/secrets/ocelot.yaml b/deployment/helm/helmfile/secrets/ocelot.yaml index 41eff134c..5b08931c7 100644 --- a/deployment/helm/helmfile/secrets/ocelot.yaml +++ b/deployment/helm/helmfile/secrets/ocelot.yaml @@ -23,15 +23,25 @@ secrets: NEO4J_USERNAME: null NEO4J_PASSWORD: null REDIS_PASSWORD: null - AWS_ACCESS_KEY_ID: ENC[AES256_GCM,data:iiN5ueqyo60VHb9e2bnhc19iGTg=,iv:zawYpKrFafgsu1+YRet1hzZf1G3a6BIlZgsh7xNADaE=,tag:rTsmm8cqei34b6cT6vn08w==,type:str] - AWS_SECRET_ACCESS_KEY: ENC[AES256_GCM,data:Zl4LRXdDh/6Q8F9RVp+3L7NXGZ0F2cgFMKPhl/TVeuD5Bhy68W5ekg==,iv:AmPoinGISrSOZdoBKdeFFXfr2hwOK4nWMnniz8K5qgU=,tag:K8Q7M7e+6G9T0Oh3Sp4OzA==,type:str] - AWS_ENDPOINT: ENC[AES256_GCM,data:/waEqUgcOmldZ+peFTNVsDQf2KrpWY8ZZMt1nT5117SkbY4=,iv:n+Kvidjb/TM4bQYKqTaFxt8GkHo02PuxEGpzgOcywr4=,tag:lrGPgCWWy3GMIcTv75IYTg==,type:str] - AWS_REGION: ENC[AES256_GCM,data:kBPpHZ8zw4PMpg==,iv:R+QZe303do37Hd/97NpS1pt9VaBE/gqZDY2/qlIvvps=,tag:0WduW8wfJXtBqlh4qfRGNA==,type:str] - AWS_BUCKET: ENC[AES256_GCM,data:0fAspN/PoRVPlSbz+qDBRUOieeC4,iv:JGJ/LyLpMymN0tpZmW6DjPT3xqXzK/KhYQsy9sgPd60=,tag:Y6PBs0916JkHRHSe7hqSMA==,type:str] + IMAGOR_SECRET: null + AWS_ACCESS_KEY_ID: null + AWS_SECRET_ACCESS_KEY: null + AWS_ENDPOINT: null + AWS_REGION: null + AWS_BUCKET: null neo4j: env: NEO4J_USERNAME: "" NEO4J_PASSWORD: "" + imagor: + env: + HTTP_LOADER_BASE_URL: null + IMAGOR_SECRET: null + AWS_ACCESS_KEY_ID: null + AWS_SECRET_ACCESS_KEY: null + AWS_ENDPOINT: null + AWS_REGION: null + AWS_BUCKET: null sops: age: - recipient: age1llp6k66265q3rzqemxpnq0x3562u20989vcjf65fl9s3hjhgcscq6mhnjw @@ -70,7 +80,7 @@ sops: aGNFeXZZRmlJM041OHdTM0pmM3BBdGMKGvFgYY1jhKwciAOZKyw0hlFVNbOk7CM7 041g17JXNV1Wk6WgMZ4w8p54RKQVaWCT4wxChy6wNNdQ3IeKgqEU2w== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-05-29T06:57:01Z" - mac: ENC[AES256_GCM,data:0eWUqVsJrolrVYFsG4aigAYSjBW35Z+64y/ZE8Az15GmI0E4p/1kZPIY6hv9SUiCD1R2Ro0nm1QsV9A/hvNeWla0EdARNnARxIphxMJWKRGwcVkFVk28Jh4g4jE/rTggWx/wLbR6bza1RLHA1wRMb1PYuLfZybsb/whN4+gTMfg=,iv:irQkLpj1+3egSsiV9HqZP4tgG1fotCOizubL42gRjSQ=,tag:DC0xzG/VqcL4ib6ijxQZnA==,type:str] + lastmodified: "2025-05-30T12:50:05Z" + mac: ENC[AES256_GCM,data:b9GHzTW9yQ2Fd+EI+bhe6D+f72ToWDwvaJfJEoIIWUC1oExU7W1uRE9tftM8iPjD9CjM/bOSH8otQYGSXcN/SM3N9DW0UnGo5yIqcz/abpLSAgXK4a5MHMFtbJ7uPlsmgEixkPo9Kc82if4qJ1lPK8LL9+W2rZC5FLTHD/a9GKU=,iv:kBUvBsxxjWlXVIzVTLvl+zGKuCeefeNWAxo7OtAoyTg=,tag:6THq7miNLRbwhqg/xt6hXw==,type:str] unencrypted_suffix: _unencrypted version: 3.10.2 diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a9e957dca..ea2bcefca 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -40,7 +40,8 @@ services: - AWS_ENDPOINT=http:/minio:9000 - AWS_REGION=local - AWS_BUCKET=ocelot - - S3_PUBLIC_GATEWAY=http:/localhost:9000 + - S3_PUBLIC_GATEWAY=http:/localhost:8000 + - IMAGOR_SECRET=mysecret volumes: - ./backend:/app @@ -83,5 +84,22 @@ services: /usr/bin/mc anonymous set-json /tmp/readonly-policy.json dockerminio/ocelot; " + imagor: + image: shumc/imagor:latest + ports: + - 8000:8000 + environment: + PORT: 8000 + IMAGOR_SECRET: mysecret # secret key for URL signature + # IMAGOR_UNSAFE: 1 # unsafe URL for testing + AWS_ACCESS_KEY_ID: h6uWJf7Earij3143YBV7 + AWS_SECRET_ACCESS_KEY: or9ZWh34BmAIqzIbJL5QpeTrey5ChGirH0mmyMdn + AWS_ENDPOINT: http:/minio:9000 + S3_FORCE_PATH_STYLE: 1 + S3_LOADER_BUCKET: ocelot # enable S3 loader by specifying bucket + S3_STORAGE_BUCKET: ocelot # enable S3 storage by specifying bucket + S3_RESULT_STORAGE_BUCKET: ocelot # enable S3 result storage by specifying bucket + HTTP_LOADER_BASE_URL: http://minio:9000 + volumes: minio_data: diff --git a/webapp/components/BadgeSelection.vue b/webapp/components/BadgeSelection.vue index a6554d779..66acfce75 100644 --- a/webapp/components/BadgeSelection.vue +++ b/webapp/components/BadgeSelection.vue @@ -7,7 +7,7 @@ @click="handleBadgeClick(badge, index)" >
- +
{{ badge.description }}
diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index 0fdf065c3..d05c3dd1f 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -8,7 +8,7 @@ :class="{ selectable: selectionMode && index > 0, selected: selectedIndex === index }" @click="handleBadgeClick(index)" > - +
diff --git a/webapp/components/Chat/Chat.vue b/webapp/components/Chat/Chat.vue index 3a52982a5..efae168e5 100644 --- a/webapp/components/Chat/Chat.vue +++ b/webapp/components/Chat/Chat.vue @@ -335,7 +335,7 @@ export default { ;[...this.messages, ...Message].forEach((m) => { if (m.senderId !== this.currentUser.id) m.seen = true m.date = new Date(m.date).toDateString() - m.avatar = this.$filters.proxyApiUrl(m.avatar) + m.avatar = m.avatar?.w320 msgs[m.indexId] = m }) this.messages = msgs.filter(Boolean) @@ -408,7 +408,7 @@ export default { const fixedRoom = { ...room, index: room.lastMessage ? room.lastMessage.date : room.createdAt, - avatar: this.$filters.proxyApiUrl(room.avatar), + avatar: room.avatar?.w320, lastMessage: room.lastMessage ? { ...room.lastMessage, @@ -416,7 +416,7 @@ export default { } : null, users: room.users.map((u) => { - return { ...u, username: u.name, avatar: this.$filters.proxyApiUrl(u.avatar?.url) } + return { ...u, username: u.name, avatar: u.avatar?.w320 } }), } if (!fixedRoom.avatar) { diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 2d4d428de..7b36d5072 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -12,7 +12,7 @@