Support chat backgrounds

This commit is contained in:
N Pex 2023-10-18 15:05:50 +00:00
parent ea8522d8a3
commit 4e9aecc304
4 changed files with 97 additions and 56 deletions

View file

@ -59,5 +59,18 @@ The app loads runtime configutation from the server at "./config.json" and merge
* Insert the resulting config blob into the "shortCodeStickers" value of the config file (assets/config.json) * Insert the resulting config blob into the "shortCodeStickers" value of the config file (assets/config.json)
* Rearrange order of sticker packs by editing the config blob above. * Rearrange order of sticker packs by editing the config blob above.
### Chat backgrounds
Chat backgrounds can be set using the **chat_backgrounds** config value. It can be set per room type, "direct", "invite" and "public". If no background is set for the current room type, the app will also check if a default "all" value has any backgrounds specified.
Backgrounds are entered as an array. Which of the backgrounds in the array is used for a given room is calculated from the room ID, so that it is constant across the lifetime of the room.
```
chat_backgrounds: {
"direct": ["https://example.com/dm1.png", "data:image/png;base64,yadiyada..."],
"all": ["/default_background.png"]
}
```
### Attributions ### Attributions
Sounds from [Notification Sounds](https://notificationsounds.com) Sounds from [Notification Sounds](https://notificationsounds.com)

View file

@ -8,7 +8,7 @@ $admin-fg: white;
body { body {
--v-app-background: $app-background; --v-app-background: $app-background;
--v-background-color: white; --v-background-color: rgba(255, 255, 255, 0.8);
--v-foreground-color: black; --v-foreground-color: black;
--v-secondary-color: #242424; --v-secondary-color: #242424;
--v-divider-color: #eeeeee; --v-divider-color: #eeeeee;
@ -233,20 +233,15 @@ body {
} }
} }
.input-area {
background-color: #e2e2e2;
margin: 0;
padding-left: $chat-standard-padding-s;
padding-right: $chat-standard-padding-s;
}
.input-area-outer { .input-area-outer {
position: relative; position: relative;
background-color: #ffffff; background-color: var(--v-background-color);
margin: 0; margin: 0;
margin-bottom: -10px;
padding-left: 2 * $chat-standard-padding-s; padding-left: 2 * $chat-standard-padding-s;
padding-right: 2 * $chat-standard-padding-s; padding-right: 2 * $chat-standard-padding-s;
padding-top: 0; padding-top: 0;
padding-bottom: 10px;
&.reply-to { &.reply-to {
padding: 0; padding: 0;
@ -283,9 +278,9 @@ body {
background-color: white; background-color: white;
border: 1px solid #d4d4d4; border: 1px solid #d4d4d4;
border-radius: 32px; border-radius: 32px;
margin-bottom: 10px;
@media #{map-get($display-breakpoints, 'sm-and-down')} { @media #{map-get($display-breakpoints, 'sm-and-down')} {
margin-bottom: 2px; margin-bottom: 12px;
} }
} }
.input-area-button { .input-area-button {
@ -423,7 +418,7 @@ body {
} }
position: relative; position: relative;
.bubble { .bubble {
background-color: #ededed; background-color: rgba(#ededed,0.8);
border-radius: 0px 10px 10px 10px; border-radius: 0px 10px 10px 10px;
[dir="rtl"] & { [dir="rtl"] & {
border-radius: 10px 0px 10px 0px; border-radius: 10px 0px 10px 0px;
@ -437,7 +432,7 @@ body {
max-width: 70%; max-width: 70%;
} }
&.from-admin .bubble { &.from-admin .bubble {
background-color: $admin-bg; background-color: rgba($admin-bg,0.8);
} }
.audio-bubble { .audio-bubble {
width: 70%; width: 70%;
@ -516,7 +511,7 @@ body {
} }
position: relative; position: relative;
.bubble { .bubble {
background-color: #e5e5e5; background-color: rgba(#e5e5e5,0.8);
border-radius: 10px 10px 0 10px; border-radius: 10px 10px 0 10px;
[dir="rtl"] & { [dir="rtl"] & {
border-radius: 10px 10px 10px 0px; border-radius: 10px 10px 10px 0px;
@ -527,7 +522,7 @@ body {
max-width: 70%; max-width: 70%;
} }
.audio-bubble { .audio-bubble {
background-color: #e5e5e5; background-color: rgba(#e5e5e5,0.8);
border-radius: 10px 10px 0 10px; border-radius: 10px 10px 0 10px;
[dir="rtl"] & { [dir="rtl"] & {
border-radius: 10px 10px 10px 0px; border-radius: 10px 10px 10px 0px;
@ -539,13 +534,6 @@ body {
width: 70%; width: 70%;
height: 50px; height: 50px;
} }
.video2-bubble {
background-color: #e5e5e5;
border-radius: 10px 10px 0 10px;
[dir="rtl"] & {
border-radius: 10px 10px 10px 0px;
}
}
.bubble.image-bubble { .bubble.image-bubble {
padding: 0px; padding: 0px;
display: inline-block; display: inline-block;
@ -824,11 +812,8 @@ body {
.read-marker { .read-marker {
margin-left: 20px; margin-left: 20px;
margin-right: 20px; margin-right: 20px;
height: 1px;
width: calc(100% - 40px); width: calc(100% - 40px);
line-height: var(--v-theme-title-featured-line-height); line-height: var(--v-theme-title-featured-line-height);
position: absolute;
bottom: 0;
font-family: sans-serif; font-family: sans-serif;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
@ -837,19 +822,18 @@ body {
/* identical to box height, or 14px */ /* identical to box height, or 14px */
letter-spacing: 0.29px; letter-spacing: 0.29px;
color: #c0c0c0; color: #c0c0c0;
background-color: #c0c0c0;
text-align: center; text-align: center;
&::after { display: flex;
position: absolute; align-items: center;
top: -4px; & div.text {
background: white; flex: 0 0 auto;
transform: translate(-50%, 0); padding-left: 10px;
[dir="rtl"] & { padding-right: 10px;
transform: translate(50%, 0); }
} & div.line {
padding-left: 4px; background: #c0c0c0;
padding-right: 4px; height: 1px;
content: attr(title); flex: 1 1 auto;
} }
} }
@ -858,7 +842,6 @@ body {
margin-right: 20px; margin-right: 20px;
margin-top: 20px; margin-top: 20px;
margin-bottom: 20px; margin-bottom: 20px;
height: 1px;
line-height: var(--v-theme-title-featured-line-height); line-height: var(--v-theme-title-featured-line-height);
font-family: sans-serif; font-family: sans-serif;
font-style: normal; font-style: normal;
@ -868,20 +851,17 @@ body {
/* identical to box height, or 14px */ /* identical to box height, or 14px */
letter-spacing: 0.29px; letter-spacing: 0.29px;
color: black; color: black;
background-color: black; display: flex;
text-align: center; align-items: center;
position: relative; & div.text {
&::after { flex: 0 0 auto;
position: absolute;
top: -8px;
background: white;
transform: translate(-50%, 0);
[dir="rtl"] & {
transform: translate(50%, 0);
}
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
content: attr(title); }
& div.line {
background: black;
height: 1px;
flex: 1 1 auto;
} }
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="chat-root fill-height d-flex flex-column"> <div class="chat-root fill-height d-flex flex-column" :style="chatContainerStyle">
<ChatHeaderPrivate class="chat-header flex-grow-0 flex-shrink-0" <ChatHeaderPrivate class="chat-header flex-grow-0 flex-shrink-0"
v-on:header-click="onHeaderClick" v-on:header-click="onHeaderClick"
v-on:view-room-details="viewRoomDetails" v-on:view-room-details="viewRoomDetails"
@ -62,7 +62,7 @@
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()"> <div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline --> <!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker" :title="dayForEvent(event)" /> <div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()"> <div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
<div class="message-wrapper" v-on:touchstart=" <div class="message-wrapper" v-on:touchstart="
@ -82,8 +82,7 @@
/> />
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> --> <!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> --> <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker" <div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
:title="$t('message.unread_messages')" />
</div> </div>
</div> </div>
</div> </div>
@ -727,7 +726,11 @@ export default {
}, },
isDirectRoom() { isDirectRoom() {
return this.room.getJoinRule() == "invite" && this.joinedAndInvitedMembers.length == 2; return this.room && this.room.getJoinRule() == "invite" && this.joinedAndInvitedMembers.length == 2;
},
isPublicRoom() {
return this.room && this.room.getJoinRule() == "public";
}, },
showCreatedRoomWelcomeHeader() { showCreatedRoomWelcomeHeader() {
@ -736,6 +739,51 @@ export default {
showDirectChatWelcomeHeader() { showDirectChatWelcomeHeader() {
return !this.hideDirectChatWelcomeHeader && this.roomCreatedByUsRecently && this.isDirectRoom; return !this.hideDirectChatWelcomeHeader && this.roomCreatedByUsRecently && this.isDirectRoom;
},
chatContainerStyle() {
if (this.$config.chat_backgrounds && this.room && this.roomId) {
const roomType = this.isDirectRoom ? "direct" : this.isPublicRoom ? "public" : "invite";
let backgrounds = this.$config.chat_backgrounds[roomType] || this.$config.chat_backgrounds["all"];
if (backgrounds) {
const numBackgrounds = backgrounds.length;
// If we have several backgrounds set, use the room ID to calculate
// an int hash value, then take mod of that to select a background to use.
// That way, we always get the same one, since room IDs don't change.
// From: https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
const hashCode = function (s) {
var hash = 0,
i, chr;
if (s.length === 0) return hash;
for (i = 0; i < s.length; i++) {
chr = s.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
// Adapted from: https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
const validUrl = function (s) {
let url;
try {
url = new URL(s, window.location);
} catch (err) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:";
}
const index = Math.abs(hashCode(this.roomId)) % numBackgrounds;
const background = backgrounds[index];
if (background && validUrl(background)) {
return "background-image: url(" + background + ");background-repeat: repeat";
}
}
}
return "";
} }
}, },

View file

@ -18,7 +18,7 @@
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"> <div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()"> <div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline --> <!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dateForEvent(event)" /> <div v-if="showDayMarkerBeforeEvent(event)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()"> <div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
<div class="message-wrapper"> <div class="message-wrapper">