Upload/Download progress; Mobile view
This commit is contained in:
parent
6b6bbdc142
commit
c1f24e3d41
35 changed files with 491 additions and 180 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "pulsar-web",
|
"name": "pulsar-web",
|
||||||
"version": "0.6.0",
|
"version": "0.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulsar-web",
|
"name": "pulsar-web",
|
||||||
"version": "0.6.0",
|
"version": "0.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solidjs/router": "^0.15.4",
|
"@solidjs/router": "^0.15.4",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "pulsar-web",
|
"name": "pulsar-web",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,30 @@
|
||||||
import type { Component } from "solid-js";
|
import type { Component } from "solid-js";
|
||||||
import { IChannelBarProps } from "./types";
|
import { IChannelBarProps } from "./types";
|
||||||
|
import { LeftIcon, UserIcon } from "../../icons";
|
||||||
|
import { resetActiveChannel } from "../../store/channel";
|
||||||
|
import { setMembersOpenMobile } from "../../store/app";
|
||||||
|
|
||||||
const ChannelBar: Component<IChannelBarProps> = (props: IChannelBarProps) => {
|
const ChannelBar: Component<IChannelBarProps> = (props: IChannelBarProps) => {
|
||||||
return (
|
return (
|
||||||
<div class="absolute w-full top-0 z-10">
|
<div class="absolute w-full flex flex-row top-0 z-10 bg-stone-900/25 backdrop-blur-md h-16 shadow-bar">
|
||||||
<div class="flex flex-col justify-center bg-stone-900/25 backdrop-blur-md h-16 w-full shadow-bar px-5">
|
<div
|
||||||
|
class="block lg:hidden h-full ml-1 w-16 p-5 cursor-pointer"
|
||||||
|
onClick={resetActiveChannel}
|
||||||
|
>
|
||||||
|
<LeftIcon />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col justify-center px-5">
|
||||||
<h2 class="text-sm font-bold">
|
<h2 class="text-sm font-bold">
|
||||||
{props.name ? `# ${props.name}` : undefined}
|
{props.name ? `# ${props.name}` : undefined}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xs">{props.description}</p>
|
<p class="text-xs">{props.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="block lg:hidden h-full ml-1 w-16 p-5 cursor-pointer"
|
||||||
|
onClick={() => setMembersOpenMobile(true)}
|
||||||
|
>
|
||||||
|
<UserIcon />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const FileInput: Component<IFileInputProps> = (props: IFileInputProps) => {
|
||||||
class={`bg-stone-800 h-40 w-40 p-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
class={`bg-stone-800 h-40 w-40 p-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class={`relative inline-block bg-stone-900 input w-full h-full p-0 focus:border-none outline-none border-2 border-stone-700 cursor-pointer ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
class={`relative inline-block bg-stone-900 input w-full h-full p-0 border-none outline-2 outline-stone-700 cursor-pointer ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||||
>
|
>
|
||||||
{props.picture ? pictureHtml() : iconOnlyHtml()}
|
{props.picture ? pictureHtml() : iconOnlyHtml()}
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,58 @@
|
||||||
import { type Component } from "solid-js";
|
import { JSXElement, type Component } from "solid-js";
|
||||||
import { IFilePreviewProps } from "./types";
|
import { IFilePreviewProps } from "./types";
|
||||||
import { Dynamic } from "solid-js/web";
|
import { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
const FilePreview: Component<IFilePreviewProps> = (
|
const FilePreview: Component<IFilePreviewProps> = (
|
||||||
props: IFilePreviewProps,
|
props: IFilePreviewProps,
|
||||||
) => {
|
) => {
|
||||||
|
const iconHtml = (): JSXElement => (
|
||||||
|
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
||||||
|
<div class="w-12">
|
||||||
|
<Dynamic component={props.icon} />
|
||||||
|
</div>
|
||||||
|
<p class="w-full text-center px-2 truncate">{props.filename}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const pictureHtml = (): JSXElement => (
|
||||||
|
<div class="avatar w-full h-full">
|
||||||
|
<img
|
||||||
|
class={props.rounded ? "rounded-full" : "rounded-xl"}
|
||||||
|
src={props.picture}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadHtml = (): JSXElement => (
|
||||||
|
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
||||||
|
<div
|
||||||
|
class="radial-progress"
|
||||||
|
style={{
|
||||||
|
"--value": props.downloadProgress ?? 0,
|
||||||
|
"--size": "4rem",
|
||||||
|
"--thickness": "7px",
|
||||||
|
}}
|
||||||
|
role="progressbar"
|
||||||
|
>
|
||||||
|
{(props.downloadProgress ?? 0).toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
<p class="w-full text-center px-2 truncate">{props.filename}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`bg-stone-800 p-2 ${props.allowResize ? "min-h-40 min-w-40 max-h-fit max-w-82" : "h-40 w-40"} ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
class={`bg-stone-800 p-2 ${props.allowResize ? "min-h-40 min-w-40 max-h-fit max-w-82" : "h-40 w-40"} ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class={`relative inline-block bg-stone-900 w-full h-full p-0 border-2 border-stone-700 focus:border-none outline-none ${props.clickable ? "cursor-pointer" : "cursor-default"} ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
class={`relative inline-block bg-stone-900 w-full h-full p-0 outline-2 outline-stone-700 ${props.clickable && props.downloadProgress === undefined ? "cursor-pointer" : "cursor-default"} ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||||
>
|
>
|
||||||
{props.picture ? (
|
{props.downloadProgress === undefined
|
||||||
<div class="avatar w-full h-full">
|
? props.picture
|
||||||
<img
|
? pictureHtml()
|
||||||
class={
|
: iconHtml()
|
||||||
props.rounded ? "rounded-full" : "rounded-xl"
|
: downloadHtml()}
|
||||||
}
|
{props.clickable && props.downloadProgress === undefined ? (
|
||||||
src={props.picture}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
|
||||||
<div class="w-12">
|
|
||||||
<Dynamic component={props.icon} />
|
|
||||||
</div>
|
|
||||||
<p class="w-full text-center px-2 truncate">
|
|
||||||
{props.filename}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{props.clickable ? (
|
|
||||||
<div
|
<div
|
||||||
class={`absolute inset-0 flex items-center justify-center bg-black/50 text-white transition-opacity opacity-0 hover:opacity-100 ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
class={`absolute inset-0 flex items-center justify-center bg-black/50 text-white transition-opacity opacity-0 hover:opacity-100 ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||||
onClick={() => props.onClick?.(props.id)}
|
onClick={() => props.onClick?.(props.id)}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ interface IFilePreviewProps {
|
||||||
outline?: boolean;
|
outline?: boolean;
|
||||||
allowResize?: boolean;
|
allowResize?: boolean;
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
|
downloadProgress?: number;
|
||||||
clickIcon?: (props: IconParameters) => JSXElement;
|
clickIcon?: (props: IconParameters) => JSXElement;
|
||||||
onClick?: (id: string | number) => void;
|
onClick?: (id: string | number) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
const HomeCard: Component<IHomeCardProps> = (props: IHomeCardProps) => {
|
const HomeCard: Component<IHomeCardProps> = (props: IHomeCardProps) => {
|
||||||
return (
|
return (
|
||||||
<a class="w-60 cursor-pointer" onClick={props.onClick}>
|
<a class="cursor-pointer" onClick={props.onClick}>
|
||||||
<div class="card outline-2 bg-stone-800 outline-stone-500 hover:outline-stone-100 w-60 h-60">
|
<div class="card transition-all border-2 bg-stone-800 border-stone-500 hover:border-stone-100 w-60 h-60">
|
||||||
<div class="flex flex-col h-full gap-1 m-6">
|
<div class="flex flex-col h-full gap-1 m-6">
|
||||||
<div class="w-20">
|
<div class="w-20">
|
||||||
<Dynamic component={props.icon} />
|
<Dynamic component={props.icon} />
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,20 @@ import { FilePreview } from "../FilePreview";
|
||||||
import { fetchFile } from "../../services/file";
|
import { fetchFile } from "../../services/file";
|
||||||
import { getActiveCommunity } from "../../store/community";
|
import { getActiveCommunity } from "../../store/community";
|
||||||
import { Input } from "../Input";
|
import { Input } from "../Input";
|
||||||
|
import { IAttachment } from "../../store/file";
|
||||||
|
|
||||||
const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
const [getEditedText, setEditedText] = createSignal<string>(props.message);
|
const [getEditedText, setEditedText] = createSignal<string>(
|
||||||
|
props.message.text,
|
||||||
|
);
|
||||||
|
const [getDownloadProgress, setDownloadProgress] = createSignal<
|
||||||
|
Map<number, number>
|
||||||
|
>(new Map<number, number>());
|
||||||
|
|
||||||
const avatarHtml = (): JSXElement => (
|
const avatarHtml = (): JSXElement => (
|
||||||
<div
|
<div
|
||||||
class="avatar cursor-pointer"
|
class="avatar cursor-pointer"
|
||||||
onClick={() => props.onProfileClick?.(props.userId)}
|
onClick={() => props.onProfileClick?.(props.message.ownerId)}
|
||||||
>
|
>
|
||||||
{props.avatar ? (
|
{props.avatar ? (
|
||||||
<div class="w-10 h-10 rounded-full">
|
<div class="w-10 h-10 rounded-full">
|
||||||
|
|
@ -35,7 +41,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onUpdateMessage = () => {
|
const onUpdateMessage = () => {
|
||||||
props.onMessageEdit?.(props.messageId, getEditedText());
|
props.onMessageEdit?.(props.message.id, getEditedText());
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownloadAttachment = (attachmentId: string | number) => {
|
const onDownloadAttachment = (attachmentId: string | number) => {
|
||||||
|
|
@ -44,9 +50,36 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateProgress = (chunkIndex: number, totalChunks: number) => {
|
||||||
|
const fileProgress = (chunkIndex + 1) / totalChunks;
|
||||||
|
|
||||||
|
if (fileProgress === 1) {
|
||||||
|
setDownloadProgress((original) => {
|
||||||
|
const next = new Map(original);
|
||||||
|
next.delete(Number(attachmentId));
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setDownloadProgress((original) => {
|
||||||
|
const next = new Map(original);
|
||||||
|
next.set(Number(attachmentId), fileProgress * 100);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const community = getActiveCommunity();
|
const community = getActiveCommunity();
|
||||||
if (community) {
|
if (community) {
|
||||||
fetchFile(community.id, attachmentId.toString());
|
setDownloadProgress((original) => {
|
||||||
|
const next = new Map(original);
|
||||||
|
next.set(Number(attachmentId), 0);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchFile(community.id, attachmentId.toString(), updateProgress);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -77,11 +110,14 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!attachment.mimetype.startsWith("image/")) {
|
if (!attachment.mimetype.startsWith("image/")) {
|
||||||
|
const progress = getDownloadProgress().get(Number(attachment.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilePreview
|
<FilePreview
|
||||||
id={attachmentId}
|
id={attachmentId}
|
||||||
icon={FileIcon}
|
icon={FileIcon}
|
||||||
filename={attachment.filename}
|
filename={attachment.filename}
|
||||||
|
downloadProgress={progress}
|
||||||
clickable={true}
|
clickable={true}
|
||||||
clickIcon={DownloadIcon}
|
clickIcon={DownloadIcon}
|
||||||
onClick={onDownloadAttachment}
|
onClick={onDownloadAttachment}
|
||||||
|
|
@ -103,7 +139,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
<FilePreview
|
<FilePreview
|
||||||
id={attachmentId}
|
id={attachmentId}
|
||||||
picture={URL.createObjectURL(attachment.fullFile)}
|
picture={URL.createObjectURL(attachment.fullFile)}
|
||||||
allowResize={props.attachments.length === 1}
|
allowResize={props.message.attachments.length === 1}
|
||||||
clickable={true}
|
clickable={true}
|
||||||
clickIcon={ZoomIcon}
|
clickIcon={ZoomIcon}
|
||||||
onClick={onOpenAttachment}
|
onClick={onOpenAttachment}
|
||||||
|
|
@ -113,7 +149,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
|
|
||||||
const attachmentsHtml = (): JSXElement => (
|
const attachmentsHtml = (): JSXElement => (
|
||||||
<div class="mt-2 flex flex-row flex-wrap gap-2">
|
<div class="mt-2 flex flex-row flex-wrap gap-2">
|
||||||
{props.attachments.map(mapAttachment)}
|
{props.message.attachments.map(mapAttachment)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -123,14 +159,18 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeHtml = (): JSXElement => (
|
const statusHtml = (): JSXElement => (
|
||||||
<div class="flex-1 text-[0.6rem] opacity-40 text-end text-nowrap">
|
<div class="h-full flex flex-col justify-between text-[0.6rem] opacity-40">
|
||||||
{new Date(props.time).toLocaleTimeString()}
|
<div class="text-nowrap">
|
||||||
|
{props.message.notSentYet
|
||||||
|
? "Sending..."
|
||||||
|
: new Date(props.message.creationDate).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const editingHtml = (): JSXElement => (
|
const editingHtml = (): JSXElement => (
|
||||||
<div class="">
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={getEditedText()}
|
value={getEditedText()}
|
||||||
onChange={setEditedText}
|
onChange={setEditedText}
|
||||||
|
|
@ -141,9 +181,10 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
|
|
||||||
const messageHtml = (): JSXElement => (
|
const messageHtml = (): JSXElement => (
|
||||||
<>
|
<>
|
||||||
{props.message.length === 0 ? undefined : props.decryptionStatus ? (
|
{props.message.text.length === 0 ? undefined : props.message
|
||||||
|
.decryptionStatus ? (
|
||||||
<p class="list-col-wrap text-xs">
|
<p class="list-col-wrap text-xs">
|
||||||
{props.editing ? editingHtml() : props.message}
|
{props.editing ? editingHtml() : props.message.text}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="list-col-wrap text-xs italic opacity-75">
|
<p class="list-col-wrap text-xs italic opacity-75">
|
||||||
|
|
@ -157,16 +198,18 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||||
<li
|
<li
|
||||||
class={`flex list-row hover:bg-stone-700 after:hidden ${props.plain ? "py-1" : "py-2 mt-3"} ${props.plain ? "" : "before:absolute before:top-0 before:left-2 before:right-2 before:h-px before:bg-stone-700 first:before:hidden"}`}
|
class={`flex list-row hover:bg-stone-700 after:hidden ${props.plain ? "py-1" : "py-2 mt-3"} ${props.plain ? "" : "before:absolute before:top-0 before:left-2 before:right-2 before:h-px before:bg-stone-700 first:before:hidden"}`}
|
||||||
onContextMenu={(e) =>
|
onContextMenu={(e) =>
|
||||||
props.onMessageRightClick?.(props.messageId, e)
|
props.onMessageRightClick?.(props.message.id, e)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{props.plain ? undefined : avatarHtml()}
|
{props.plain ? undefined : avatarHtml()}
|
||||||
<div class={`w-full pr-14 ${props.plain ? "pl-14" : ""}`}>
|
<div class={`w-full pr-4 ${props.plain ? "pl-14" : ""}`}>
|
||||||
{props.plain ? undefined : nameHtml()}
|
{props.plain ? undefined : nameHtml()}
|
||||||
{messageHtml()}
|
{messageHtml()}
|
||||||
{props.attachments.length > 0 ? attachmentsHtml() : undefined}
|
{props.message.attachments.length > 0
|
||||||
|
? attachmentsHtml()
|
||||||
|
: undefined}
|
||||||
</div>
|
</div>
|
||||||
{timeHtml()}
|
{statusHtml()}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
|
import { IMessage } from "../../store/message";
|
||||||
|
|
||||||
interface IMessageProps {
|
interface IMessageProps {
|
||||||
messageId: string;
|
message: IMessage;
|
||||||
message: string;
|
|
||||||
userId: string;
|
|
||||||
username: string;
|
username: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
attachments: string[];
|
|
||||||
time: number;
|
|
||||||
plain: boolean;
|
plain: boolean;
|
||||||
decryptionStatus: boolean;
|
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
onProfileClick?: (userId: string) => void;
|
onProfileClick?: (userId: string) => void;
|
||||||
onMessageRightClick?: (id: string, event: MouseEvent) => void;
|
onMessageRightClick?: (id: string, event: MouseEvent) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { createSignal, type Component, type JSXElement } from "solid-js";
|
import {
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
type Component,
|
||||||
|
type JSXElement,
|
||||||
|
} from "solid-js";
|
||||||
import { IMessageBarProps, MESSAGE_BAR_OPTIONS, OptionIcon } from "./types";
|
import { IMessageBarProps, MESSAGE_BAR_OPTIONS, OptionIcon } from "./types";
|
||||||
import {
|
import {
|
||||||
AttachmentIcon,
|
AttachmentIcon,
|
||||||
|
LocationIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PollIcon,
|
PollIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
|
@ -24,9 +30,19 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
||||||
|
|
||||||
const options = new Map<MESSAGE_BAR_OPTIONS, OptionIcon>([
|
const options = new Map<MESSAGE_BAR_OPTIONS, OptionIcon>([
|
||||||
[MESSAGE_BAR_OPTIONS.ATTACH_FILES, AttachmentIcon],
|
[MESSAGE_BAR_OPTIONS.ATTACH_FILES, AttachmentIcon],
|
||||||
|
[MESSAGE_BAR_OPTIONS.ATTACH_LOCATION, LocationIcon],
|
||||||
[MESSAGE_BAR_OPTIONS.CREATE_POLL, PollIcon],
|
[MESSAGE_BAR_OPTIONS.CREATE_POLL, PollIcon],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.uploadProgress === undefined) {
|
||||||
|
setAttachmentOpen(false);
|
||||||
|
|
||||||
|
setFiles([]);
|
||||||
|
setFilePreviews([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleEnter = (e: KeyboardEvent) => {
|
const handleEnter = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
onSend();
|
onSend();
|
||||||
|
|
@ -95,10 +111,6 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
||||||
props.onSend?.(getFiles());
|
props.onSend?.(getFiles());
|
||||||
|
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
setAttachmentOpen(false);
|
|
||||||
|
|
||||||
setFiles([]);
|
|
||||||
setFilePreviews([]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapOption = (
|
const mapOption = (
|
||||||
|
|
@ -126,7 +138,7 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
||||||
id={index}
|
id={index}
|
||||||
icon={AttachmentIcon}
|
icon={AttachmentIcon}
|
||||||
filename={getFiles().at(index)?.name}
|
filename={getFiles().at(index)?.name}
|
||||||
clickable={true}
|
clickable={!props.uploadProgress}
|
||||||
clickIcon={TrashIcon}
|
clickIcon={TrashIcon}
|
||||||
picture={filePreview}
|
picture={filePreview}
|
||||||
onClick={onFileRemove}
|
onClick={onFileRemove}
|
||||||
|
|
@ -139,7 +151,48 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
||||||
<div class="bg-stone-800/50 backdrop-blur-lg h-48 w-full shadow-bar rounded-3xl p-4 flex flex-row gap-2 overflow-x-scroll">
|
<div class="bg-stone-800/50 backdrop-blur-lg h-48 w-full shadow-bar rounded-3xl p-4 flex flex-row gap-2 overflow-x-scroll">
|
||||||
{getFilePreviews().map(mapFile)}
|
{getFilePreviews().map(mapFile)}
|
||||||
<div class="w-40">
|
<div class="w-40">
|
||||||
<FileInput multifile={true} onChange={onFileAdd} />
|
<FileInput
|
||||||
|
multifile={true}
|
||||||
|
onChange={(fileList) =>
|
||||||
|
!props.uploadProgress
|
||||||
|
? onFileAdd?.(fileList)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadingHtml = (): JSXElement => (
|
||||||
|
<div class="absolute w-full bottom-18 p-4 z-10">
|
||||||
|
<div class="flex flex-row gap-2 bg-stone-800/50 backdrop-blur-lg h-16 w-64 shadow-bar rounded-full mx-auto p-1">
|
||||||
|
<div
|
||||||
|
class="radial-progress"
|
||||||
|
style={{
|
||||||
|
"--value": props.uploadProgress?.total,
|
||||||
|
"--size": "3.5rem",
|
||||||
|
"--thickness": "7px",
|
||||||
|
}}
|
||||||
|
role="progressbar"
|
||||||
|
>
|
||||||
|
{props.uploadProgress?.total.toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col justify-center items-center">
|
||||||
|
{props.uploadProgress?.total === 0 ? (
|
||||||
|
<span>Starting upload...</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
Uploading file {props.uploadProgress?.fileIndex}/
|
||||||
|
{props.uploadProgress?.fileCount}
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{props.uploadProgress?.total === 0 ? undefined : (
|
||||||
|
<span>
|
||||||
|
{props.uploadProgress?.fileProgress.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -147,54 +200,66 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getAttachmentOpen() ? attachmentHtml() : undefined}
|
{getAttachmentOpen()
|
||||||
<div class="absolute w-full bottom-0 p-4 z-10">
|
? props.uploadProgress
|
||||||
<div class="absolute h-16 w-full pr-8">
|
? uploadingHtml()
|
||||||
|
: attachmentHtml()
|
||||||
|
: undefined}
|
||||||
|
<div class="absolute w-full bottom-0 z-10">
|
||||||
|
<div class="absolute h-24 w-full p-4">
|
||||||
<div class="bg-stone-800/25 backdrop-blur-lg rounded-full h-full w-full"></div>
|
<div class="bg-stone-800/25 backdrop-blur-lg rounded-full h-full w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-stone-800/25 h-16 shadow-bar p-2 flex flex-row gap-2 rounded-full">
|
<div class="p-4">
|
||||||
<div
|
<div class="bg-stone-800/25 h-16 shadow-bar p-2 flex flex-row gap-2 rounded-full">
|
||||||
class={`group dropdown dropdown-top ${getDropdownOpen() ? "" : "dropdown-close"}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
class={`group dropdown dropdown-top ${getDropdownOpen() ? "" : "dropdown-close"}`}
|
||||||
role="button"
|
|
||||||
class={`btn btn-neutral bg-stone-950/50 backdrop-blur-lg w-12 p-0 h-full rounded-full transition-transform ${getDropdownOpen() ? "rotate-45" : ""}`}
|
|
||||||
onClick={openDropdown}
|
|
||||||
>
|
>
|
||||||
<div class="w-6">
|
<div
|
||||||
<PlusIcon />
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
class={`btn ${props.uploadProgress ? "btn-disabled" : ""} btn-neutral bg-stone-950/50 backdrop-blur-lg w-12 p-0 h-full rounded-full transition-transform ${getDropdownOpen() ? "rotate-45" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
props.uploadProgress
|
||||||
|
? undefined
|
||||||
|
: openDropdown?.()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="w-6">
|
||||||
|
<PlusIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
tabindex="-1"
|
||||||
|
class="dropdown-content -translate-x-2 -translate-y-4 z-10 w-64 animate-none"
|
||||||
|
>
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
{[...options].map(mapOption)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
tabindex="-1"
|
|
||||||
class="dropdown-content -translate-x-2 -translate-y-4 z-10 w-64 animate-none"
|
|
||||||
>
|
|
||||||
<ul class="flex flex-col gap-2">
|
|
||||||
{[...options].map(mapOption)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="bg-stone-900/50 backdrop-blur-lg input w-full h-full rounded-full focus:border-none outline-none">
|
<label class="bg-stone-900/50 backdrop-blur-lg input w-full h-full rounded-full focus:border-none outline-none">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Send a message..."
|
placeholder="Send a message..."
|
||||||
value={props.text}
|
value={props.text}
|
||||||
onInput={(e) =>
|
disabled={props.uploadProgress !== undefined}
|
||||||
props.onChangeText?.(e.currentTarget.value)
|
onInput={(e) =>
|
||||||
}
|
props.onChangeText?.(e.currentTarget.value)
|
||||||
onKeyDown={handleEnter}
|
}
|
||||||
/>
|
onKeyDown={handleEnter}
|
||||||
</label>
|
/>
|
||||||
<button
|
</label>
|
||||||
class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral w-12 p-0 h-full rounded-full"
|
<button
|
||||||
onClick={onSend}
|
class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral w-12 p-0 h-full rounded-full"
|
||||||
>
|
disabled={props.uploadProgress !== undefined}
|
||||||
<div class="w-5">
|
onClick={onSend}
|
||||||
<UpIcon />
|
>
|
||||||
</div>
|
<div class="w-5">
|
||||||
</button>
|
<UpIcon />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,30 @@ import { IconParameters } from "../../icons";
|
||||||
|
|
||||||
interface IMessageBarProps {
|
interface IMessageBarProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
uploadProgress?: IUploadProgress;
|
||||||
onChangeText?: (text: string) => void;
|
onChangeText?: (text: string) => void;
|
||||||
onSend?: (files: File[]) => void;
|
onSend?: (files: File[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IUploadProgress {
|
||||||
|
total: number;
|
||||||
|
fileName: string;
|
||||||
|
fileIndex: number;
|
||||||
|
fileCount: number;
|
||||||
|
fileProgress: number;
|
||||||
|
}
|
||||||
|
|
||||||
type OptionIcon = (props: IconParameters) => JSXElement;
|
type OptionIcon = (props: IconParameters) => JSXElement;
|
||||||
|
|
||||||
enum MESSAGE_BAR_OPTIONS {
|
enum MESSAGE_BAR_OPTIONS {
|
||||||
ATTACH_FILES = "Attach files",
|
ATTACH_FILES = "Attach files",
|
||||||
|
ATTACH_LOCATION = "Attach location",
|
||||||
CREATE_POLL = "Create a poll",
|
CREATE_POLL = "Create a poll",
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type IMessageBarProps, type OptionIcon, MESSAGE_BAR_OPTIONS };
|
export {
|
||||||
|
type IMessageBarProps,
|
||||||
|
type IUploadProgress,
|
||||||
|
type OptionIcon,
|
||||||
|
MESSAGE_BAR_OPTIONS,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
||||||
if (props.avatar) {
|
if (props.avatar) {
|
||||||
return (
|
return (
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<div class="w-12 rounded-lg">
|
<div class="min-w-12 w-12 rounded-lg">
|
||||||
<img src={props.avatar} />
|
<img src={props.avatar} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,7 +19,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
||||||
if (props.icon) {
|
if (props.icon) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`bg-stone-700 w-12 rounded-lg p-${props.iconPadding ? props.iconPadding : 1}`}
|
class={`bg-stone-700 min-w-12 w-12 rounded-lg p-${props.iconPadding ? props.iconPadding : 1}`}
|
||||||
>
|
>
|
||||||
<Dynamic component={props.icon} />
|
<Dynamic component={props.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -51,7 +51,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title font-semibold flex flex-row items-center gap-4 p-1">
|
<div class="collapse-title font-semibold flex flex-row items-center gap-4 p-1">
|
||||||
{pictureHtml()}
|
{pictureHtml()}
|
||||||
{props.title}
|
<span class="truncate">{props.title}</span>
|
||||||
{infoHtml()}
|
{infoHtml()}
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content text-sm font-semibold">
|
<div class="collapse-content text-sm font-semibold">
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { state } from "../../store/state";
|
||||||
import { decryptData, encryptData, importKey } from "../crypto";
|
import { decryptData, encryptData, importKey } from "../crypto";
|
||||||
import { DB_STORE, IDatabaseItem } from "./types";
|
import { DB_STORE, IDatabaseItem } from "./types";
|
||||||
|
|
||||||
|
const DB_NAME = "pulsardb";
|
||||||
const DB_VERSION = 1;
|
const DB_VERSION = 1;
|
||||||
|
|
||||||
const openDB = async (name: string, store: string): Promise<IDBDatabase> => {
|
const openDB = async (name: string): Promise<IDBDatabase> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(name, DB_VERSION);
|
const request = indexedDB.open(name, DB_VERSION);
|
||||||
|
|
||||||
|
|
@ -27,12 +28,16 @@ const openDB = async (name: string, store: string): Promise<IDBDatabase> => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDB = async (store: string): Promise<IDBDatabase> => {
|
const getDB = async (): Promise<IDBDatabase> => {
|
||||||
return await openDB("pulsardb", store);
|
return await openDB(DB_NAME);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDB = async () => {
|
||||||
|
return indexedDB.deleteDatabase(DB_NAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dbSave = async (store: string, item: IDatabaseItem): Promise<void> => {
|
const dbSave = async (store: string, item: IDatabaseItem): Promise<void> => {
|
||||||
const db = await getDB(store);
|
const db = await getDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(store, "readwrite");
|
const tx = db.transaction(store, "readwrite");
|
||||||
|
|
@ -47,7 +52,7 @@ const dbLoad = async (
|
||||||
store: string,
|
store: string,
|
||||||
id: IDBValidKey,
|
id: IDBValidKey,
|
||||||
): Promise<IDatabaseItem | undefined> => {
|
): Promise<IDatabaseItem | undefined> => {
|
||||||
const db = await getDB(store);
|
const db = await getDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(store, "readonly");
|
const tx = db.transaction(store, "readonly");
|
||||||
|
|
@ -110,6 +115,7 @@ const dbLoadEncrypted = async <T>(
|
||||||
export {
|
export {
|
||||||
openDB,
|
openDB,
|
||||||
getDB,
|
getDB,
|
||||||
|
deleteDB,
|
||||||
dbSave,
|
dbSave,
|
||||||
dbLoad,
|
dbLoad,
|
||||||
importKey,
|
importKey,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ import {
|
||||||
generateIv,
|
generateIv,
|
||||||
} from "../crypto";
|
} from "../crypto";
|
||||||
|
|
||||||
const CHUNK_SIZE = 1024 * 1024 * 5;
|
const MIN_CHUNK_SIZE = 1024 * 1024 * 1;
|
||||||
|
const MAX_CHUNK_SIZE = 1024 * 1024 * 5;
|
||||||
|
|
||||||
const uploadUserAvatar = async (filename: string, file: File) => {
|
const uploadUserAvatar = async (filename: string, file: File) => {
|
||||||
const data = await uploadUserAvatarApi({ filename: filename, file: file });
|
const data = await uploadUserAvatarApi({ filename: filename, file: file });
|
||||||
|
|
@ -153,7 +154,13 @@ const createAttachment = async (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAttachment({ ...data, fullFile: undefined });
|
setAttachment({
|
||||||
|
...data,
|
||||||
|
filename: filename,
|
||||||
|
mimetype: mimetype,
|
||||||
|
size: size,
|
||||||
|
fullFile: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
return data.id;
|
return data.id;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -215,6 +222,7 @@ const fetchChunks = async (
|
||||||
attachmentId: string,
|
attachmentId: string,
|
||||||
chunkCount: number,
|
chunkCount: number,
|
||||||
mimetype?: string,
|
mimetype?: string,
|
||||||
|
onChunkDownloaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||||
) => {
|
) => {
|
||||||
const file: ArrayBuffer[] = [];
|
const file: ArrayBuffer[] = [];
|
||||||
|
|
||||||
|
|
@ -229,6 +237,8 @@ const fetchChunks = async (
|
||||||
if (!status) {
|
if (!status) {
|
||||||
decryptionSuccessful = false;
|
decryptionSuccessful = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChunkDownloaded?.(i, chunkCount);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -242,13 +252,23 @@ const fetchChunks = async (
|
||||||
setAttachmentDecryptionStatus(attachmentId, decryptionSuccessful);
|
setAttachmentDecryptionStatus(attachmentId, decryptionSuccessful);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFile = async (communityId: string, attachmentId: string) => {
|
const fetchFile = async (
|
||||||
|
communityId: string,
|
||||||
|
attachmentId: string,
|
||||||
|
onChunkDownloaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||||
|
) => {
|
||||||
const attachment = await fetchAttachment(attachmentId, communityId);
|
const attachment = await fetchAttachment(attachmentId, communityId);
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchChunks(communityId, attachmentId, attachment.chunks.length);
|
await fetchChunks(
|
||||||
|
communityId,
|
||||||
|
attachmentId,
|
||||||
|
attachment.chunks.length,
|
||||||
|
undefined,
|
||||||
|
onChunkDownloaded,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedAttachment = state.file.attachments[attachmentId];
|
const updatedAttachment = state.file.attachments[attachmentId];
|
||||||
if (updatedAttachment?.fullFile && updatedAttachment.decryptionStatus) {
|
if (updatedAttachment?.fullFile && updatedAttachment.decryptionStatus) {
|
||||||
|
|
@ -264,7 +284,11 @@ const fetchFile = async (communityId: string, attachmentId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMedia = async (communityId: string, attachmentId: string) => {
|
const fetchMedia = async (
|
||||||
|
communityId: string,
|
||||||
|
attachmentId: string,
|
||||||
|
onChunkDownloaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||||
|
) => {
|
||||||
const attachment = await fetchAttachment(attachmentId, communityId);
|
const attachment = await fetchAttachment(attachmentId, communityId);
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -279,6 +303,7 @@ const fetchMedia = async (communityId: string, attachmentId: string) => {
|
||||||
attachmentId,
|
attachmentId,
|
||||||
attachment.chunks.length,
|
attachment.chunks.length,
|
||||||
attachment.mimetype,
|
attachment.mimetype,
|
||||||
|
onChunkDownloaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -328,12 +353,22 @@ const uploadChunks = async (
|
||||||
communityId: string,
|
communityId: string,
|
||||||
attachmentId: string,
|
attachmentId: string,
|
||||||
file: File,
|
file: File,
|
||||||
|
onChunkUploaded?: (chunkIndex: number, totalChunks: number) => void,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
const fileSizeMb = file.size / 1024 / 1024;
|
||||||
|
|
||||||
|
let chunkSize = MIN_CHUNK_SIZE;
|
||||||
|
if (fileSizeMb >= 100) {
|
||||||
|
chunkSize = MAX_CHUNK_SIZE;
|
||||||
|
} else if (fileSizeMb >= 20) {
|
||||||
|
chunkSize = file.size / 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
for (let index = 0; index < totalChunks; index++) {
|
for (let index = 0; index < totalChunks; index++) {
|
||||||
const start = index * CHUNK_SIZE;
|
const start = index * chunkSize;
|
||||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
const blob = file.slice(start, end);
|
const blob = file.slice(start, end);
|
||||||
|
|
||||||
const uploaded = await uploadChunk(
|
const uploaded = await uploadChunk(
|
||||||
|
|
@ -345,6 +380,7 @@ const uploadChunks = async (
|
||||||
if (!uploaded) {
|
if (!uploaded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
onChunkUploaded?.(index, totalChunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ const fetchMessage = async (id: string, communityId: string) => {
|
||||||
const createMessage = async (
|
const createMessage = async (
|
||||||
communityId: string,
|
communityId: string,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
|
ownerId: string,
|
||||||
text: string,
|
text: string,
|
||||||
attachments: string[],
|
attachments: string[],
|
||||||
replyToId?: string,
|
replyToId?: string,
|
||||||
|
|
@ -56,6 +57,24 @@ const createMessage = async (
|
||||||
}
|
}
|
||||||
const [encryptedMessage, iv] = encrypted;
|
const [encryptedMessage, iv] = encrypted;
|
||||||
|
|
||||||
|
const tempUUID = crypto.randomUUID();
|
||||||
|
|
||||||
|
setMessage({
|
||||||
|
id: tempUUID,
|
||||||
|
iv: iv,
|
||||||
|
text: text,
|
||||||
|
attachments: attachments,
|
||||||
|
edited: false,
|
||||||
|
reactions: [],
|
||||||
|
creationDate: new Date().getTime(),
|
||||||
|
editHistory: [],
|
||||||
|
replyToId: replyToId,
|
||||||
|
channelId: channelId,
|
||||||
|
ownerId: ownerId,
|
||||||
|
decryptionStatus: true,
|
||||||
|
notSentYet: true,
|
||||||
|
});
|
||||||
|
|
||||||
const data = await createMessageApi({
|
const data = await createMessageApi({
|
||||||
text: encryptedMessage,
|
text: encryptedMessage,
|
||||||
iv: iv,
|
iv: iv,
|
||||||
|
|
@ -67,15 +86,14 @@ const createMessage = async (
|
||||||
if (typeof data.error === "string") {
|
if (typeof data.error === "string") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteChannelMessage(channelId, tempUUID);
|
||||||
|
|
||||||
setMessage({
|
setMessage({
|
||||||
...data,
|
...data,
|
||||||
text: text,
|
text: text,
|
||||||
decryptionStatus: true,
|
decryptionStatus: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
attachments.forEach((attachmentId) => {
|
|
||||||
deleteAttachment(attachmentId);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMessage = async (id: string, text: string, communityId: string) => {
|
const updateMessage = async (id: string, text: string, communityId: string) => {
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,20 @@ const parseMessage = (data: string): SocketMessage | null => {
|
||||||
const handleMessage = (message: SocketMessage) => {
|
const handleMessage = (message: SocketMessage) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case SocketMessageTypes.SET_MESSAGE: {
|
case SocketMessageTypes.SET_MESSAGE: {
|
||||||
const communityId =
|
const channel = state.channel.channels[message.payload.channelId];
|
||||||
state.channel.channels[message.payload.channelId]?.communityId;
|
const communityId = channel?.communityId;
|
||||||
if (!communityId) {
|
if (!channel || !communityId) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (channel.messages) {
|
||||||
|
const exists = Object.values(channel.messages).some(
|
||||||
|
(msg) => msg?.iv === message.payload.message.iv,
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
decryptMessage(
|
decryptMessage(
|
||||||
communityId,
|
communityId,
|
||||||
message.payload.message.text,
|
message.payload.message.text,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ const resetContextMenuOpenId = () => {
|
||||||
setState("app", "contextMenuOpenId", null);
|
setState("app", "contextMenuOpenId", null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setMembersOpenMobile = (membersOpenMobile: boolean) => {
|
||||||
|
setState("app", "membersOpenMobile", membersOpenMobile);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
setAnnouncement,
|
setAnnouncement,
|
||||||
setHomeOpen,
|
setHomeOpen,
|
||||||
|
|
@ -66,4 +70,5 @@ export {
|
||||||
setCreateInviteOpen,
|
setCreateInviteOpen,
|
||||||
setContextMenuOpenId,
|
setContextMenuOpenId,
|
||||||
resetContextMenuOpenId,
|
resetContextMenuOpenId,
|
||||||
|
setMembersOpenMobile,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ interface IAppState {
|
||||||
announcement: IAnnouncement;
|
announcement: IAnnouncement;
|
||||||
dialogsOpen: IDialogsOpen;
|
dialogsOpen: IDialogsOpen;
|
||||||
contextMenuOpenId: string | null;
|
contextMenuOpenId: string | null;
|
||||||
|
membersOpenMobile: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAnnouncement {
|
interface IAnnouncement {
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,15 @@ const deleteMessage = () => {
|
||||||
setState("message", "message", undefined);
|
setState("message", "message", undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { setMessage, deleteMessage };
|
const deleteChannelMessage = (messageId: string, channelId: string) => {
|
||||||
|
setState(
|
||||||
|
"channel",
|
||||||
|
"channels",
|
||||||
|
channelId,
|
||||||
|
"messages",
|
||||||
|
messageId,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { setMessage, deleteMessage, deleteChannelMessage };
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ interface IMessage {
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
decryptionStatus?: boolean;
|
decryptionStatus?: boolean;
|
||||||
|
notSentYet?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type IMessageState, type IMessage };
|
export { type IMessageState, type IMessage };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { IState } from "./types";
|
import { IState } from "./types";
|
||||||
|
|
||||||
const [state, setState] = createStore<IState>({
|
const defaultState: IState = {
|
||||||
app: {
|
app: {
|
||||||
homeOpen: true,
|
homeOpen: true,
|
||||||
announcement: {
|
announcement: {
|
||||||
|
|
@ -18,6 +18,7 @@ const [state, setState] = createStore<IState>({
|
||||||
createInviteOpen: false,
|
createInviteOpen: false,
|
||||||
},
|
},
|
||||||
contextMenuOpenId: null,
|
contextMenuOpenId: null,
|
||||||
|
membersOpenMobile: false,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
registerSuccess: undefined,
|
registerSuccess: undefined,
|
||||||
|
|
@ -52,6 +53,12 @@ const [state, setState] = createStore<IState>({
|
||||||
attachments: {},
|
attachments: {},
|
||||||
chunks: {},
|
chunks: {},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
export { state, setState };
|
const [state, setState] = createStore<IState>(defaultState);
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
setState(defaultState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { state, setState, resetState };
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ const AppView: Component = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log(state.auth.loggedIn);
|
|
||||||
if (state.auth.loggedIn === false) {
|
if (state.auth.loggedIn === false) {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
} else if (state.auth.loggedIn === true) {
|
} else if (state.auth.loggedIn === true) {
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,9 @@ const ChannelView: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-stone-900 w-80 shadow-panel z-20 h-full relative">
|
<div
|
||||||
|
class={`${getActiveChannel() ? "hidden lg:block" : "block flex-1 lg:flex-none"} lg:w-80 bg-stone-900 shadow-panel z-20 h-full relative`}
|
||||||
|
>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
visible={listMenu.getVisible()}
|
visible={listMenu.getVisible()}
|
||||||
position={listMenu.getPos()}
|
position={listMenu.getPos()}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { ChannelBar } from "../../components/ChannelBar";
|
import { ChannelBar } from "../../components/ChannelBar";
|
||||||
import { MessageBar } from "../../components/MessageBar";
|
import { IUploadProgress, MessageBar } from "../../components/MessageBar";
|
||||||
import { Message } from "../../components/Message";
|
import { Message } from "../../components/Message";
|
||||||
import { state } from "../../store/state";
|
import { state } from "../../store/state";
|
||||||
import { getActiveChannel, IChannel, setText } from "../../store/channel";
|
import { getActiveChannel, IChannel, setText } from "../../store/channel";
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
import { IMessage } from "../../store/message";
|
import { IMessage } from "../../store/message";
|
||||||
import {
|
import {
|
||||||
createAttachment,
|
createAttachment,
|
||||||
|
fetchMedia,
|
||||||
finishAttachment,
|
finishAttachment,
|
||||||
uploadChunks,
|
uploadChunks,
|
||||||
} from "../../services/file";
|
} from "../../services/file";
|
||||||
|
|
@ -38,6 +39,7 @@ import {
|
||||||
} from "../../icons";
|
} from "../../icons";
|
||||||
import { getUserBestRoleId } from "../../services/role";
|
import { getUserBestRoleId } from "../../services/role";
|
||||||
import { IRole } from "../../store/role";
|
import { IRole } from "../../store/role";
|
||||||
|
import { getLoggedInUser } from "../../store/user";
|
||||||
|
|
||||||
const ChatView: Component = () => {
|
const ChatView: Component = () => {
|
||||||
const menu = useContextMenu<string>("chat-message-menu");
|
const menu = useContextMenu<string>("chat-message-menu");
|
||||||
|
|
@ -45,6 +47,8 @@ const ChatView: Component = () => {
|
||||||
const [getEditingMessages, setEditingMessages] = createSignal<Set<string>>(
|
const [getEditingMessages, setEditingMessages] = createSignal<Set<string>>(
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
|
const [getAttachmentUploadProgress, setAttachmentUploadProgress] =
|
||||||
|
createSignal<IUploadProgress | undefined>();
|
||||||
|
|
||||||
const editingMenuItems = createMemo<IContextMenuItem[]>(() => {
|
const editingMenuItems = createMemo<IContextMenuItem[]>(() => {
|
||||||
if (getEditingMessages().has(menu.getData() ?? "")) {
|
if (getEditingMessages().has(menu.getData() ?? "")) {
|
||||||
|
|
@ -180,7 +184,8 @@ const ChatView: Component = () => {
|
||||||
|
|
||||||
const channel = channelInfo();
|
const channel = channelInfo();
|
||||||
const community = getActiveCommunity();
|
const community = getActiveCommunity();
|
||||||
if (!channel?.id || !community?.id) {
|
const owner = getLoggedInUser();
|
||||||
|
if (!channel?.id || !community?.id || !owner?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,35 +197,72 @@ const ChatView: Component = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentIds: string[] = [];
|
const attachmentIds: string[] = [];
|
||||||
for (const file of files) {
|
|
||||||
const attachmentId = await createAttachment(
|
|
||||||
community.id,
|
|
||||||
file.name,
|
|
||||||
file.type,
|
|
||||||
file.size,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (attachmentId) {
|
if (files.length > 0) {
|
||||||
attachmentIds.push(attachmentId);
|
setAttachmentUploadProgress({
|
||||||
const uploaded = await uploadChunks(
|
total: 0,
|
||||||
|
fileName: files[0].name,
|
||||||
|
fileIndex: 0,
|
||||||
|
fileCount: 0,
|
||||||
|
fileProgress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [index, file] of files.entries()) {
|
||||||
|
const updateProgress = (
|
||||||
|
chunkIndex: number,
|
||||||
|
totalChunks: number,
|
||||||
|
) => {
|
||||||
|
const totalProgress = index / files.length;
|
||||||
|
const fileProgress = (chunkIndex + 1) / totalChunks;
|
||||||
|
|
||||||
|
setAttachmentUploadProgress({
|
||||||
|
total:
|
||||||
|
(totalProgress + fileProgress / files.length) * 100,
|
||||||
|
fileName: file.name,
|
||||||
|
fileIndex: index + 1,
|
||||||
|
fileCount: files.length,
|
||||||
|
fileProgress: fileProgress * 100,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachmentId = await createAttachment(
|
||||||
community.id,
|
community.id,
|
||||||
attachmentId,
|
file.name,
|
||||||
file,
|
file.type,
|
||||||
|
file.size,
|
||||||
);
|
);
|
||||||
if (uploaded) {
|
|
||||||
await finishAttachment(attachmentId);
|
if (attachmentId) {
|
||||||
|
attachmentIds.push(attachmentId);
|
||||||
|
const uploaded = await uploadChunks(
|
||||||
|
community.id,
|
||||||
|
attachmentId,
|
||||||
|
file,
|
||||||
|
updateProgress,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploaded) {
|
||||||
|
await finishAttachment(attachmentId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAttachmentUploadProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(channel.id, "");
|
setText(channel.id, "");
|
||||||
createMessage(
|
await createMessage(
|
||||||
community.id,
|
community.id,
|
||||||
channel.id,
|
channel.id,
|
||||||
|
owner.id,
|
||||||
text ?? "",
|
text ?? "",
|
||||||
attachmentIds,
|
attachmentIds,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
attachmentIds.forEach((attachmentId) => {
|
||||||
|
fetchMedia(community.id, attachmentId);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveMessage = (messageId: string | undefined) => {
|
const onRemoveMessage = (messageId: string | undefined) => {
|
||||||
|
|
@ -316,16 +358,11 @@ const ChatView: Component = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Message
|
<Message
|
||||||
messageId={message.id ?? ""}
|
message={message}
|
||||||
message={message.text}
|
|
||||||
userId={message.ownerId}
|
|
||||||
username={user.nickname ?? user.username ?? ""}
|
username={user.nickname ?? user.username ?? ""}
|
||||||
color={bestRole?.color}
|
color={bestRole?.color}
|
||||||
avatar={getUserAvatarUrl(user.avatar)}
|
avatar={getUserAvatarUrl(user.avatar)}
|
||||||
attachments={message.attachments}
|
|
||||||
time={message.creationDate}
|
|
||||||
plain={isPlainMessage(message, allMessages[index - 1])}
|
plain={isPlainMessage(message, allMessages[index - 1])}
|
||||||
decryptionStatus={message.decryptionStatus ?? false}
|
|
||||||
editing={getEditingMessages().has(message.id)}
|
editing={getEditingMessages().has(message.id)}
|
||||||
onProfileClick={onProfileClick}
|
onProfileClick={onProfileClick}
|
||||||
onMessageRightClick={onMessageRightClick}
|
onMessageRightClick={onMessageRightClick}
|
||||||
|
|
@ -335,7 +372,9 @@ const ChatView: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-stone-800 flex-1 z-0 relative">
|
<div
|
||||||
|
class={`${getActiveChannel() && !state.app.membersOpenMobile ? "block" : "hidden lg:block"} bg-stone-800 flex-1 z-0 relative`}
|
||||||
|
>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
visible={menu.getVisible()}
|
visible={menu.getVisible()}
|
||||||
position={menu.getPos()}
|
position={menu.getPos()}
|
||||||
|
|
@ -357,6 +396,7 @@ const ChatView: Component = () => {
|
||||||
{channelInfo() ? (
|
{channelInfo() ? (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
text={channelInfo()?.text ?? ""}
|
text={channelInfo()?.text ?? ""}
|
||||||
|
uploadProgress={getAttachmentUploadProgress()}
|
||||||
onChangeText={onChangeMessageText}
|
onChangeText={onChangeMessageText}
|
||||||
onSend={onSendMessage}
|
onSend={onSendMessage}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -70,14 +70,14 @@ const CommunitySettingsModalView: Component<
|
||||||
{getActiveCommunity()?.name} Settings
|
{getActiveCommunity()?.name} Settings
|
||||||
</h3>
|
</h3>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="flex flex-row gap-4 px-4">
|
<div class="flex flex-col lg:flex-row gap-4 px-4 items-center lg:items-stretch">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h3 class="text-lg font-bold text-center mb-4">
|
<h3 class="text-lg font-bold text-center mb-4">
|
||||||
Categories
|
Categories
|
||||||
</h3>
|
</h3>
|
||||||
{[...pages].map(mapPageButton)}
|
{[...pages].map(mapPageButton)}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||||
{getCurrentPage()}
|
{getCurrentPage()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -244,8 +244,8 @@ const CommunitySettingsChannelsPage: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 flex flex-row gap-4">
|
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||||
<h3 class="text-lg font-bold text-center mb-4">Channels</h3>
|
<h3 class="text-lg font-bold text-center mb-4">Channels</h3>
|
||||||
{channels().map(mapChannel)}
|
{channels().map(mapChannel)}
|
||||||
{channels().length > 0 ? (
|
{channels().length > 0 ? (
|
||||||
|
|
@ -261,7 +261,7 @@ const CommunitySettingsChannelsPage: Component = () => {
|
||||||
Add new Channel
|
Add new Channel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||||
<div class="flex-1">{optionsHtml()}</div>
|
<div class="flex-1">{optionsHtml()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -221,12 +221,12 @@ const CommunitySettingsMembersPage: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 flex flex-row gap-4">
|
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||||
<h3 class="text-lg font-bold text-center mb-4">Members</h3>
|
<h3 class="text-lg font-bold text-center mb-4">Members</h3>
|
||||||
{members().map(mapMember)}
|
{members().map(mapMember)}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||||
<div class="flex-1">{optionsHtml()}</div>
|
<div class="flex-1">{optionsHtml()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -285,8 +285,8 @@ const CommunitySettingsRolesPage: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 flex flex-row gap-4">
|
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||||
<h3 class="text-lg font-bold text-center mb-4">Roles</h3>
|
<h3 class="text-lg font-bold text-center mb-4">Roles</h3>
|
||||||
{roles().map(mapRole)}
|
{roles().map(mapRole)}
|
||||||
{roles().length > 0 ? <div class="divider"></div> : undefined}
|
{roles().length > 0 ? <div class="divider"></div> : undefined}
|
||||||
|
|
@ -300,7 +300,7 @@ const CommunitySettingsRolesPage: Component = () => {
|
||||||
Add new Role
|
Add new Role
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||||
<div class="flex-1">{optionsHtml()}</div>
|
<div class="flex-1">{optionsHtml()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
getCommunityAvatarUrl,
|
getCommunityAvatarUrl,
|
||||||
removeCommunityMember,
|
removeCommunityMember,
|
||||||
} from "../../services/community";
|
} from "../../services/community";
|
||||||
import { resetActiveChannel } from "../../store/channel";
|
import { getActiveChannel, resetActiveChannel } from "../../store/channel";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
IContextMenuItem,
|
IContextMenuItem,
|
||||||
|
|
@ -108,7 +108,9 @@ const CommunityView: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col bg-stone-950 w-16 h-full shadow-panel z-30 p-2 gap-2">
|
<div
|
||||||
|
class={`${getActiveChannel() ? "hidden lg:flex" : "flex"} flex flex-col bg-stone-950 w-16 h-full shadow-panel z-30 p-2 gap-2`}
|
||||||
|
>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
visible={menu.getVisible()}
|
visible={menu.getVisible()}
|
||||||
position={menu.getPos()}
|
position={menu.getPos()}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import { HomeCard } from "../../components/HomeCard";
|
||||||
import { setAddCommunityOpen, setSettingsOpen } from "../../store/app";
|
import { setAddCommunityOpen, setSettingsOpen } from "../../store/app";
|
||||||
import { removeSession } from "../../services/session";
|
import { removeSession } from "../../services/session";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { state } from "../../store/state";
|
import { resetState, state } from "../../store/state";
|
||||||
import { resetLoggedIn, resetAuthSession } from "../../store/auth";
|
import { resetLoggedIn, resetAuthSession } from "../../store/auth";
|
||||||
|
import { deleteDB } from "../../services/database";
|
||||||
|
|
||||||
const HomeView: Component = () => {
|
const HomeView: Component = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -26,12 +27,15 @@ const HomeView: Component = () => {
|
||||||
resetLoggedIn();
|
resetLoggedIn();
|
||||||
resetAuthSession();
|
resetAuthSession();
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
deleteDB();
|
||||||
|
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 flex flex-row bg-stone-900 shadow-panel z-0">
|
<div class="bg-stone-900 flex-1 flex flex-col lg:flex-row items-center justify-center">
|
||||||
<div class="flex-1 flex flex-row items-center justify-center gap-8">
|
<div class="flex flex-col lg:flex-row items-center justify-start gap-8 p-8 w-full lg:w-fit overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
||||||
<HomeCard
|
<HomeCard
|
||||||
title="Find a Community"
|
title="Find a Community"
|
||||||
description="Find or create a new Community to chat"
|
description="Find or create a new Community to chat"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createEffect, createSignal, onMount, type Component } from "solid-js";
|
import { createEffect, createSignal, onMount, type Component } from "solid-js";
|
||||||
import { state } from "../../store/state";
|
import { setState, state } from "../../store/state";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { fetchLogin, fetchRefresh } from "../../services/auth";
|
import { fetchLogin, fetchRefresh } from "../../services/auth";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,12 @@ import {
|
||||||
IContextMenuItem,
|
IContextMenuItem,
|
||||||
useContextMenu,
|
useContextMenu,
|
||||||
} from "../../components/ContextMenu";
|
} from "../../components/ContextMenu";
|
||||||
import { DownIcon, TrashIcon, UpIcon } from "../../icons";
|
import { DownIcon, LeftIcon, TrashIcon, UpIcon } from "../../icons";
|
||||||
import { getActiveCommunity } from "../../store/community";
|
import { getActiveCommunity } from "../../store/community";
|
||||||
import { IUser } from "../../store/user";
|
import { IUser } from "../../store/user";
|
||||||
import { IRole } from "../../store/role";
|
import { IRole } from "../../store/role";
|
||||||
import { getUserBestRoleId } from "../../services/role";
|
import { getUserBestRoleId } from "../../services/role";
|
||||||
|
import { setMembersOpenMobile } from "../../store/app";
|
||||||
|
|
||||||
const MemberView: Component = () => {
|
const MemberView: Component = () => {
|
||||||
const roleMenu = useContextMenu<string>("channel-role-menu");
|
const roleMenu = useContextMenu<string>("channel-role-menu");
|
||||||
|
|
@ -243,7 +244,9 @@ const MemberView: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-stone-900 w-64 shadow-panel z-20 relative">
|
<div
|
||||||
|
class={`${state.app.membersOpenMobile ? "block w-full" : "hidden "} lg:block bg-stone-900 lg:w-64 shadow-panel z-20 relative`}
|
||||||
|
>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
visible={roleMenu.getVisible()}
|
visible={roleMenu.getVisible()}
|
||||||
position={roleMenu.getPos()}
|
position={roleMenu.getPos()}
|
||||||
|
|
@ -257,7 +260,13 @@ const MemberView: Component = () => {
|
||||||
items={memberMenuItems}
|
items={memberMenuItems}
|
||||||
/>
|
/>
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<ul class="h-full list flex flex-col p-2 gap-1 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
<div
|
||||||
|
class="block lg:hidden ml-1 w-16 p-5 cursor-pointer"
|
||||||
|
onClick={() => setMembersOpenMobile(false)}
|
||||||
|
>
|
||||||
|
<LeftIcon />
|
||||||
|
</div>
|
||||||
|
<ul class="list flex flex-col p-2 gap-1 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
||||||
{roles().map(mapRole)}
|
{roles().map(mapRole)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,14 @@ const SettingsModalView: Component<ISettingsModalViewProps> = (
|
||||||
<div class="modal-box w-10/12 max-w-5xl h-11/12 2xl:h-9/12 bg-stone-950 rounded-3xl">
|
<div class="modal-box w-10/12 max-w-5xl h-11/12 2xl:h-9/12 bg-stone-950 rounded-3xl">
|
||||||
<h3 class="text-lg font-bold text-center">Settings</h3>
|
<h3 class="text-lg font-bold text-center">Settings</h3>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="flex flex-row gap-4 px-4">
|
<div class="flex flex-col lg:flex-row gap-4 px-4 items-center lg:items-stretch">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h3 class="text-lg font-bold text-center mb-4">
|
<h3 class="text-lg font-bold text-center mb-4">
|
||||||
Categories
|
Categories
|
||||||
</h3>
|
</h3>
|
||||||
{[...pages].map(mapPageButton)}
|
{[...pages].map(mapPageButton)}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||||
{getCurrentPage()}
|
{getCurrentPage()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -146,12 +146,12 @@ const SettingsCommunitiesPage: Component = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 flex flex-row gap-4">
|
<div class="flex-1 flex flex-col lg:flex-row gap-4 w-full">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 items-center lg:items-stretch">
|
||||||
<h3 class="text-lg font-bold text-center mb-4">Communities</h3>
|
<h3 class="text-lg font-bold text-center mb-4">Communities</h3>
|
||||||
{communityIds().map(mapCommunity)}
|
{communityIds().map(mapCommunity)}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-vertical lg:divider-horizontal"></div>
|
||||||
<div class="flex-1">{optionsHtml()}</div>
|
<div class="flex-1">{optionsHtml()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ import {
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { fetchUserSessions } from "../../../../services/user";
|
import { fetchUserSessions } from "../../../../services/user";
|
||||||
import { state } from "../../../../store/state";
|
import { resetState, state } from "../../../../store/state";
|
||||||
import { RichSettingsItem } from "../../../../components/RichSettingsItem";
|
import { RichSettingsItem } from "../../../../components/RichSettingsItem";
|
||||||
import { DeviceIcon, MinusIcon, TrashIcon } from "../../../../icons";
|
import { DeviceIcon, MinusIcon, TrashIcon } from "../../../../icons";
|
||||||
import { removeSession } from "../../../../services/session";
|
import { removeSession } from "../../../../services/session";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
import { deleteDB } from "../../../../services/database";
|
||||||
|
|
||||||
const SettingsSessionPage: Component = () => {
|
const SettingsSessionPage: Component = () => {
|
||||||
const [getSelectedSessionId, setSelectedSessionId] = createSignal<
|
const [getSelectedSessionId, setSelectedSessionId] = createSignal<
|
||||||
|
|
@ -43,6 +44,9 @@ const SettingsSessionPage: Component = () => {
|
||||||
removeSession(sessionId);
|
removeSession(sessionId);
|
||||||
|
|
||||||
if (sessionId === state.auth.session?.id) {
|
if (sessionId === state.auth.session?.id) {
|
||||||
|
resetState();
|
||||||
|
deleteDB();
|
||||||
|
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue