Merge branch 'main' into improved-context-menu
This commit is contained in:
commit
ecfa85c0ac
55 changed files with 1030 additions and 297 deletions
44
components/Avatar.vue
Normal file
44
components/Avatar.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<NuxtImg v-if="displayAvatar"
|
||||
class="display-avatar"
|
||||
:src="displayAvatar"
|
||||
:alt="displayName" />
|
||||
<Icon v-else
|
||||
name="lucide:user"
|
||||
:alt="displayName" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NuxtImg } from '#components';
|
||||
import type { GuildMemberResponse, UserResponse } from '~/types/interfaces';
|
||||
|
||||
const props = defineProps<{
|
||||
user?: UserResponse,
|
||||
member?: GuildMemberResponse,
|
||||
}>();
|
||||
|
||||
|
||||
let displayName: string
|
||||
let displayAvatar: string | null
|
||||
|
||||
const user = props.user || props.member?.user
|
||||
|
||||
if (user) {
|
||||
displayName = getDisplayName(user, props.member)
|
||||
|
||||
if (user.avatar) {
|
||||
displayAvatar = user.avatar
|
||||
} else if (!isCanvasBlocked()){
|
||||
displayAvatar = generateDefaultIcon(displayName, user.uuid)
|
||||
} else {
|
||||
displayAvatar = null
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.display-avatar {
|
||||
border-radius: var(--pfp-radius);
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div v-if="isCurrentChannel" class="channel-list-link-container rounded-corners current-channel" tabindex="0">
|
||||
<div v-if="isCurrentChannel" class="channel-list-link-container rounded-corners current-channel" tabindex="0" :title="props.name">
|
||||
<NuxtLink class="channel-list-link" :href="props.href" tabindex="-1">
|
||||
# {{ props.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div v-else class="channel-list-link-container rounded-corners" tabindex="0">
|
||||
<div v-else class="channel-list-link-container rounded-corners" tabindex="0" :title="props.name">
|
||||
<NuxtLink class="channel-list-link" :href="props.href" tabindex="-1">
|
||||
# {{ props.name }}
|
||||
</NuxtLink>
|
||||
|
@ -25,6 +25,8 @@ const isCurrentChannel = props.uuid == props.currentUuid;
|
|||
color: inherit;
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.channel-list-link-container {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<template>
|
||||
<div class="member-item" @click="togglePopup" @blur="hidePopup" tabindex="0">
|
||||
<img v-if="props.member.user.avatar" class="member-avatar" :src="props.member.user.avatar" :alt="props.member.user.display_name ?? props.member.user.username" />
|
||||
<Icon v-else class="member-avatar" name="lucide:user" />
|
||||
<span class="member-display-name">{{ props.member.user.display_name || props.member.user.username }}</span>
|
||||
<Avatar :member="props.member" class="member-avatar"/>
|
||||
<span class="member-display-name">{{ getDisplayName(props.member.user, props.member) }}</span>
|
||||
<UserPopup v-if="isPopupVisible" :user="props.member.user" id="profile-popup" />
|
||||
</div>
|
||||
</template>
|
|
@ -1,27 +1,30 @@
|
|||
<template>
|
||||
<div id="middle-left-column">
|
||||
<div id="friend-sidebar">
|
||||
<div>
|
||||
<h3>Direct Messages</h3>
|
||||
</div>
|
||||
<VerticalSpacer />
|
||||
|
||||
<NuxtLink class="user-item" :href="`/me/friends`" tabindex="0">
|
||||
<Icon class="user-avatar" name="lucide:user" />
|
||||
<span class="user-display-name">Friends</span>
|
||||
</NuxtLink>
|
||||
<VerticalSpacer />
|
||||
|
||||
<div id="direct-message-list">
|
||||
<UserEntry v-for="user of friends" :user="user" :name="user.display_name || user.username"
|
||||
:href="`/me/${user.uuid}`"/>
|
||||
<ResizableSidebar width="14rem" min-width="8rem" max-width="30rem" border-sides="right" local-storage-name="middleLeftColumn">
|
||||
<div id="middle-left-column">
|
||||
<div id="friend-sidebar">
|
||||
<div>
|
||||
<h3>Direct Messages</h3>
|
||||
</div>
|
||||
<VerticalSpacer />
|
||||
|
||||
<NuxtLink class="user-item" :href="`/me`" tabindex="0">
|
||||
<Icon class="user-avatar" name="lucide:user" />
|
||||
<span class="user-display-name">Friends</span>
|
||||
</NuxtLink>
|
||||
<VerticalSpacer />
|
||||
|
||||
<div id="direct-message-list">
|
||||
<UserEntry v-for="user of friends" :user="user"
|
||||
:href="`/me/${user.uuid}`"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizableSidebar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
|
||||
import ResizableSidebar from '../UserInterface/ResizableSidebar.vue';
|
||||
|
||||
const { fetchFriends } = useApi();
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
|
||||
<div v-else>
|
||||
<UserEntry v-for="user of friends" :user="user" :name="user.display_name || user.username"
|
||||
<UserEntry v-for="user of friends" :user="user" :name="getDisplayName(user)"
|
||||
:href="`/me/${user.uuid}`"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,7 +26,7 @@
|
|||
<script lang="ts" setup>
|
||||
const { fetchFriends } = useApi();
|
||||
|
||||
const friends = await fetchFriends()
|
||||
const friends = sortUsers(await fetchFriends())
|
||||
|
||||
const props = defineProps<{
|
||||
variant: string
|
||||
|
|
|
@ -8,14 +8,10 @@
|
|||
viewBox="0 0 150 87.5" version="1.1" id="svg1"
|
||||
style="overflow: visible;">
|
||||
<defs id="defs1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="layer1"
|
||||
<g id="layer1"
|
||||
transform="translate(40,-35)">
|
||||
<g
|
||||
id="g3"
|
||||
transform="translate(-35,-20)">
|
||||
<g id="g3"
|
||||
transform="translate(-35,-20)">
|
||||
<path
|
||||
style="stroke:var(--reply-text-color);stroke-width:8;stroke-opacity:1"
|
||||
d="m 120.02168,87.850978 100.76157,2.4e-5"
|
||||
|
@ -28,16 +24,17 @@
|
|||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<MessageReply v-if="props.replyMessage" :author="props.replyMessage.user.display_name || props.replyMessage.user.username" :text="props.replyMessage?.message"
|
||||
:id="props.message.uuid" :reply-id="props.replyMessage.uuid" max-width="reply" />
|
||||
<MessageReply v-if="props.replyMessage" :id="props.message.uuid"
|
||||
:author="getDisplayName(props.replyMessage.user)"
|
||||
:text="props.replyMessage?.message"
|
||||
:reply-id="props.replyMessage.uuid" max-width="reply" />
|
||||
<div class="left-column">
|
||||
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="author?.display_name || author?.username" />
|
||||
<Icon v-else name="lucide:user" class="message-author-avatar" />
|
||||
<Avatar :user="props.author" class="message-author-avatar"/>
|
||||
</div>
|
||||
<div class="message-data">
|
||||
<div class="message-metadata">
|
||||
<span class="message-author-username" tabindex="0">
|
||||
{{ author?.display_name || author?.username }}
|
||||
<span class="message-author-username" tabindex="0" :style="`color: ${props.authorColor}`">
|
||||
{{ getDisplayName(props.author) }}
|
||||
</span>
|
||||
<span class="message-date" :title="date.toString()">
|
||||
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
|
||||
|
@ -233,7 +230,6 @@ function getDayDifference(date1: Date, date2: Date) {
|
|||
|
||||
.message-author-avatar {
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<div id="message-area">
|
||||
<div id="messages" ref="messagesElement">
|
||||
<Message v-for="(message, i) of messages" :username="message.user.display_name ?? message.user.username"
|
||||
<Message v-for="(message, i) of messages" :username="getDisplayName(message.user)"
|
||||
:text="message.message" :timestamp="messageTimestamps[message.uuid]" :img="message.user.avatar"
|
||||
:format="timeFormat" :type="messagesType[message.uuid]"
|
||||
:margin-bottom="(messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal') ?? false"
|
||||
:last="i == messages.length - 1" :message-id="message.uuid" :author="message.user" :me="me"
|
||||
:message="message" :is-reply="message.reply_to"
|
||||
:author-color="`${generateIrcColor(message.user.uuid)}`"
|
||||
:reply-message="message.reply_to ? getReplyMessage(message.reply_to) : undefined" />
|
||||
</div>
|
||||
<div id="message-box" class="rounded-corners">
|
||||
|
@ -41,6 +42,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MessageResponse, ScrollPosition, UserResponse } from '~/types/interfaces';
|
||||
import scrollToBottom from '~/utils/scrollToBottom';
|
||||
import { generateIrcColor } from '#imports';
|
||||
|
||||
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import Cropper from 'cropperjs';
|
||||
import Button from '../UserInterface/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
imageSrc: String,
|
||||
|
|
|
@ -32,13 +32,13 @@
|
|||
<script lang="ts" setup>
|
||||
import RadioButtons from '~/components/UserInterface/RadioButtons.vue';
|
||||
import type { TimeFormat } from '~/types/settings';
|
||||
import loadPreferredTheme from '~/utils/loadPreferredTheme';
|
||||
import settingSave from '~/utils/settingSave';
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const defaultThemes = runtimeConfig.public.defaultThemes
|
||||
const baseURL = runtimeConfig.app.baseURL;
|
||||
const timeFormatTextStrings = ["Auto", "12-Hour", "24-Hour"]
|
||||
let themeLinkElement: HTMLLinkElement | null = null;
|
||||
|
||||
const themes: Array<Theme> = []
|
||||
|
||||
|
@ -51,20 +51,8 @@ interface Theme {
|
|||
}
|
||||
|
||||
function changeTheme(id: string, url: string) {
|
||||
if (themeLinkElement && themeLinkElement.getAttribute('href') === `${baseURL}themes/${url}`) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingSave("selectedThemeId", id)
|
||||
|
||||
// if the theme didn't originally load for some reason, create it
|
||||
if (!themeLinkElement) {
|
||||
themeLinkElement = document.createElement('link');
|
||||
themeLinkElement.rel = 'stylesheet';
|
||||
document.head.appendChild(themeLinkElement);
|
||||
}
|
||||
|
||||
themeLinkElement.href = `${baseURL}themes/${url}`;
|
||||
loadPreferredTheme()
|
||||
}
|
||||
|
||||
async function fetchThemes() {
|
||||
|
|
11
components/Settings/AppSettings/index.ts
Normal file
11
components/Settings/AppSettings/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Appearance from './Appearance.vue';
|
||||
import Notifications from './Notifications.vue';
|
||||
import Keybinds from './Keybinds.vue';
|
||||
import Language from './Language.vue';
|
||||
|
||||
export {
|
||||
Appearance,
|
||||
Notifications,
|
||||
Keybinds,
|
||||
Language,
|
||||
}
|
|
@ -33,6 +33,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CropPopup from '~/components/Popups/CropPopup.vue';
|
||||
import UserPopup from '~/components/User/UserPopup.vue';
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
|
||||
|
@ -41,7 +42,7 @@ import type { UserResponse } from '~/types/interfaces';
|
|||
let newPfpFile: File;
|
||||
const isCropPopupVisible = ref(false);
|
||||
const cropImageSrc = ref("")
|
||||
;
|
||||
|
||||
const { fetchUser } = useAuth();
|
||||
|
||||
const user: UserResponse | undefined = await fetchUser()
|
||||
|
|
13
components/Settings/UserSettings/index.ts
Normal file
13
components/Settings/UserSettings/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Profile from './Profile.vue';
|
||||
import Account from './Account.vue';
|
||||
import Privacy from './Privacy.vue';
|
||||
import Devices from './Devices.vue';
|
||||
import Connections from './Connections.vue';
|
||||
|
||||
export {
|
||||
Profile,
|
||||
Account,
|
||||
Privacy,
|
||||
Devices,
|
||||
Connections,
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<NuxtLink class="user-item" :href="`/me/${user.uuid}`" tabindex="0">
|
||||
<img v-if="props.user.avatar" class="user-avatar" :src="props.user.avatar" :alt="props.user.display_name ?? props.user.username" />
|
||||
<Icon v-else class="user-avatar" name="lucide:user" />
|
||||
<span class="user-display-name">{{ props.user.display_name || props.user.username }}</span>
|
||||
<Avatar :user="props.user" class="user-avatar"/>
|
||||
|
||||
<span class="user-display-name">{{ getDisplayName(props.user) }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
|
@ -12,6 +12,7 @@ import type { UserResponse } from '~/types/interfaces';
|
|||
const props = defineProps<{
|
||||
user: UserResponse
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -33,8 +34,15 @@ const props = defineProps<{
|
|||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 2.3em;
|
||||
height: 2.3em;
|
||||
border-radius: 50%;
|
||||
min-width: 2.3em;
|
||||
max-width: 2.3em;
|
||||
min-width: 2.3em;
|
||||
max-height: 2.3em;
|
||||
}
|
||||
|
||||
.user-display-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div id="profile-popup">
|
||||
<img v-if="props.user.avatar" id="avatar" :src="props.user.avatar" alt="profile avatar">
|
||||
<Icon v-else id="avatar" name="lucide:user" />
|
||||
<Avatar :user="props.user" id="avatar"/>
|
||||
|
||||
<div id="cover-color"></div>
|
||||
<div id="main-body">
|
||||
<p id="display-name">
|
||||
<strong>{{ props.user.display_name }}</strong>
|
||||
<strong>{{ getDisplayName(props.user) }}</strong>
|
||||
</p>
|
||||
<p id="username-and-pronouns">
|
||||
{{ props.user.username }}
|
||||
|
@ -22,8 +21,6 @@
|
|||
<script lang="ts" setup>
|
||||
import type { UserResponse } from '~/types/interfaces';
|
||||
|
||||
const { fetchMembers } = useApi();
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserResponse
|
||||
}>();
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div @click="props.callback()" class="button" :class="props.variant + '-button'">
|
||||
<button @click="props.callback ? props.callback() : null" class="button" :class="props.variant + '-button'">
|
||||
{{ props.text }}
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const props = defineProps<{
|
||||
text: string,
|
||||
callback: CallableFunction,
|
||||
callback?: CallableFunction,
|
||||
variant?: "normal" | "scary" | "neutral",
|
||||
}>();
|
||||
|
||||
|
@ -28,6 +28,8 @@ const props = defineProps<{
|
|||
border-radius: 0.7rem;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ContextMenuInterface, ContextMenuItem } from '~/types/interfaces';
|
||||
|
||||
const props = defineProps<{ menuItems: ContextMenuItem[], cursorX: number, cursorY: number }>();
|
||||
const props = defineProps<{ menuItems: ContextMenuItem[], pointerX: number, pointerY: number }>();
|
||||
|
||||
onMounted(() => {
|
||||
const contextMenu = document.getElementById("context-menu");
|
||||
if (contextMenu) {
|
||||
contextMenu.style.left = props.cursorX.toString() + "px";
|
||||
contextMenu.style.top = props.cursorY.toString() + "px";
|
||||
contextMenu.style.left = props.pointerX.toString() + "px";
|
||||
contextMenu.style.top = props.pointerY.toString() + "px";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
140
components/UserInterface/ResizableSidebar.vue
Normal file
140
components/UserInterface/ResizableSidebar.vue
Normal file
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div ref="resizableSidebar" class="resizable-sidebar"
|
||||
:style="{
|
||||
'width': storedWidth ? `${storedWidth}px` : props.width,
|
||||
'min-width': props.minWidth,
|
||||
'max-width': props.maxWidth,
|
||||
'border': props.borderSides == 'all' ? borderStyling : undefined,
|
||||
'border-top': props.borderSides?.includes('top') ? borderStyling : undefined,
|
||||
'border-bottom': props.borderSides?.includes('bottom') ? borderStyling : undefined,
|
||||
}">
|
||||
<div v-if="props.borderSides != 'right'" class="width-resizer-bar">
|
||||
<div ref="widthResizer" class="width-resizer"></div>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-if="props.borderSides == 'right'" class="width-resizer-bar">
|
||||
<div ref="widthResizer" class="width-resizer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ContextMenuItem } from '~/types/interfaces';
|
||||
|
||||
const props = defineProps<{ width?: string, minWidth: string, maxWidth: string, borderSides: "all" | "top" | "right" | "bottom" | "left" | ("top" | "right" | "bottom" | "left")[], localStorageName?: string }>();
|
||||
|
||||
const borderStyling = ".1rem solid var(--padding-color)";
|
||||
|
||||
const resizableSidebar = ref<HTMLDivElement>();
|
||||
const widthResizer = ref<HTMLDivElement>();
|
||||
const storedWidth = ref<number>();
|
||||
|
||||
const menuItems: ContextMenuItem[] = [
|
||||
{ name: "Reset", callback: () => { resizableSidebar.value!.style.width = props.width ?? props.minWidth } }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
loadStoredWidth();
|
||||
|
||||
if (resizableSidebar.value && widthResizer.value) {
|
||||
widthResizer.value.addEventListener("pointerdown", (e) => {
|
||||
e.preventDefault();
|
||||
if (e.button == 2) {
|
||||
createContextMenu(e, menuItems);
|
||||
return
|
||||
};
|
||||
document.body.style.cursor = "ew-resize";
|
||||
function handleMove(pointer: PointerEvent) {
|
||||
if (resizableSidebar.value) {
|
||||
console.log("moving");
|
||||
console.log("pointer:", pointer);
|
||||
console.log("width:", resizableSidebar.value.style.width);
|
||||
let delta = 0;
|
||||
if (props.borderSides == 'right') {
|
||||
delta = resizableSidebar.value.getBoundingClientRect().left;
|
||||
console.log("delta:", delta);
|
||||
resizableSidebar.value.style.width = `${pointer.clientX - delta}px`;
|
||||
} else {
|
||||
delta = resizableSidebar.value.getBoundingClientRect().right;
|
||||
console.log("delta:", delta);
|
||||
resizableSidebar.value.style.width = `${delta - pointer.clientX}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("pointermove", handleMove);
|
||||
|
||||
document.addEventListener("pointerup", () => {
|
||||
console.log("pointer up");
|
||||
document.removeEventListener("pointermove", handleMove);
|
||||
console.log("removed pointermove event listener");
|
||||
document.body.style.cursor = "";
|
||||
if (resizableSidebar.value && props.localStorageName) {
|
||||
localStorage.setItem(props.localStorageName, resizableSidebar.value.style.width);
|
||||
}
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
console.log("[res] activated");
|
||||
loadStoredWidth();
|
||||
});
|
||||
|
||||
function loadStoredWidth() {
|
||||
if (props.localStorageName) {
|
||||
const storedWidthValue = localStorage.getItem(props.localStorageName);
|
||||
if (storedWidthValue) {
|
||||
storedWidth.value = parseInt(storedWidthValue) || undefined;
|
||||
console.log("[res] loaded stored width");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.resizable-sidebar > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resizable-sidebar {
|
||||
display: flex;
|
||||
background: var(--optional-channel-list-background);
|
||||
background-color: var(--sidebar-background-color);
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.width-resizer {
|
||||
width: .5rem;
|
||||
cursor: col-resize;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.width-resizer-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: var(--padding-color);
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
width: 100%;
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
}
|
||||
|
||||
.sidebar-content > :first-child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue