message date spacer
This commit is contained in:
parent
43814a0b6e
commit
73e81f0bc9
@ -94,7 +94,13 @@ export class Member implements WithEquals<Member> {
|
||||
}
|
||||
|
||||
export class Channel implements WithEquals<Channel> {
|
||||
private constructor(public readonly id: string, public readonly index: number, public readonly name: string, public readonly flavorText: string | null, public readonly source?: unknown) {}
|
||||
private constructor(
|
||||
public readonly id: string,
|
||||
public readonly index: number,
|
||||
public readonly name: string,
|
||||
public readonly flavorText: string | null,
|
||||
public readonly source?: unknown,
|
||||
) {}
|
||||
|
||||
static fromDBData(dataChannel: any): Channel {
|
||||
return new Channel(dataChannel.id, dataChannel.index, dataChannel.name, dataChannel.flavor_text, dataChannel);
|
||||
@ -109,7 +115,12 @@ export class Channel implements WithEquals<Channel> {
|
||||
}
|
||||
|
||||
equals(other: Channel) {
|
||||
return this.id === other.id && this.index === other.index && this.name === other.name && this.flavorText === other.flavorText;
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.index === other.index &&
|
||||
this.name === other.name &&
|
||||
this.flavorText === other.flavorText
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,7 +201,9 @@ export class Message implements WithEquals<Message> {
|
||||
const channel = this.channel instanceof Channel ? this.channel.name : `ch#${this.channel.id}`;
|
||||
const member = this.member instanceof Member ? this.member.displayName : `m#${this.member.id}`;
|
||||
if (this.resourceName) {
|
||||
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${this.text ? this.text + ' / ' : ''}${this.resourceName}`;
|
||||
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${
|
||||
this.text ? this.text + ' / ' : ''
|
||||
}${this.resourceName}`;
|
||||
} else {
|
||||
return `msg#${this.id} ${channel} @${formatDate(this.sent)} ${member}: ${this.text}`;
|
||||
}
|
||||
@ -222,11 +235,24 @@ export class Message implements WithEquals<Message> {
|
||||
);
|
||||
}
|
||||
|
||||
// returns true if the message happens on the next day from the last message
|
||||
isNextDay(lastMessage: Message | null): boolean {
|
||||
if (!lastMessage) return true; // this is the first message
|
||||
const msPerDay = 24 * 60 * 60 * 1000;
|
||||
const lastMessageDay = Math.floor(lastMessage.sent.getTime() / msPerDay);
|
||||
const messageDay = Math.floor(this.sent.getTime() / msPerDay);
|
||||
return messageDay !== lastMessageDay;
|
||||
}
|
||||
|
||||
// returns true if this message should be considered 'continued' from the last message
|
||||
// messages sent in quick succession (within 5 minutes) of the previous message
|
||||
// by the same sender are considered continued messages.
|
||||
isContinued(lastMessage: Message | null): boolean {
|
||||
return !!(lastMessage && lastMessage.member.id === this.member.id && this.sent.getTime() - lastMessage.sent.getTime() < 5 * 60 * 1000);
|
||||
return !!(
|
||||
lastMessage &&
|
||||
lastMessage.member.id === this.member.id &&
|
||||
this.sent.getTime() - lastMessage.sent.getTime() < 5 * 60 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
hasResource(): boolean {
|
||||
@ -236,7 +262,10 @@ export class Message implements WithEquals<Message> {
|
||||
isImageResource(): boolean {
|
||||
return !!(
|
||||
this.hasResource() &&
|
||||
(this.resourceName?.toLowerCase().endsWith('.png') || this.resourceName?.toLowerCase().endsWith('.jpg') || this.resourceName?.toLowerCase().endsWith('.jpeg') || this.resourceName?.toLowerCase().endsWith('.gif'))
|
||||
(this.resourceName?.toLowerCase().endsWith('.png') ||
|
||||
this.resourceName?.toLowerCase().endsWith('.jpg') ||
|
||||
this.resourceName?.toLowerCase().endsWith('.jpeg') ||
|
||||
this.resourceName?.toLowerCase().endsWith('.gif'))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -253,12 +282,24 @@ export class SocketConfig {
|
||||
) {}
|
||||
|
||||
static fromDBData(data: any): SocketConfig {
|
||||
return new SocketConfig(data.id, data.guild_id, data.url, data.cert, crypto.createPublicKey(data.public_key), crypto.createPrivateKey(data.private_key), data);
|
||||
return new SocketConfig(
|
||||
data.id,
|
||||
data.guild_id,
|
||||
data.url,
|
||||
data.cert,
|
||||
crypto.createPublicKey(data.public_key),
|
||||
crypto.createPrivateKey(data.private_key),
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GuildMetadata implements WithEquals<GuildMetadata> {
|
||||
public constructor(public readonly name: string, public readonly iconResourceId: string, public readonly source?: unknown) {}
|
||||
public constructor(
|
||||
public readonly name: string,
|
||||
public readonly iconResourceId: string,
|
||||
public readonly source?: unknown,
|
||||
) {}
|
||||
|
||||
static fromDBData(data: any) {
|
||||
return new GuildMetadata(data.name, data.icon_resource_id, data);
|
||||
@ -274,7 +315,13 @@ export class GuildMetadata implements WithEquals<GuildMetadata> {
|
||||
}
|
||||
|
||||
export class GuildMetadataLocal extends GuildMetadata {
|
||||
private constructor(public readonly id: number, name: string, iconResourceId: string, public readonly memberId: string | null, source?: unknown) {
|
||||
private constructor(
|
||||
public readonly id: number,
|
||||
name: string,
|
||||
iconResourceId: string,
|
||||
public readonly memberId: string | null,
|
||||
source?: unknown,
|
||||
) {
|
||||
super(name, iconResourceId, source);
|
||||
}
|
||||
|
||||
@ -300,7 +347,14 @@ export class Resource implements WithEquals<Resource> {
|
||||
}
|
||||
|
||||
export class Token implements WithEquals<Token> {
|
||||
constructor(public readonly id: string, public readonly token: string, public member: Member | { id: string } | null, public readonly created: Date, public readonly expires: Date | null, public readonly source?: unknown) {}
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly token: string,
|
||||
public member: Member | { id: string } | null,
|
||||
public readonly created: Date,
|
||||
public readonly expires: Date | null,
|
||||
public readonly source?: unknown,
|
||||
) {}
|
||||
|
||||
fill(members: Map<string, Member>) {
|
||||
if (this.member) {
|
||||
@ -312,11 +366,24 @@ export class Token implements WithEquals<Token> {
|
||||
}
|
||||
|
||||
static fromDBData(dataToken: any): Token {
|
||||
return new Token(dataToken.id, dataToken.token, dataToken.member_id ? { id: dataToken.member_id } : null, new Date(dataToken.created), dataToken.expires ? new Date(dataToken.expires) : null, dataToken);
|
||||
return new Token(
|
||||
dataToken.id,
|
||||
dataToken.token,
|
||||
dataToken.member_id ? { id: dataToken.member_id } : null,
|
||||
new Date(dataToken.created),
|
||||
dataToken.expires ? new Date(dataToken.expires) : null,
|
||||
dataToken,
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: Token) {
|
||||
return this.id === other.id && this.token === other.token && this.member?.id === other.member?.id && this.created === other.created && this.expires === other.expires;
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.token === other.token &&
|
||||
this.member?.id === other.member?.id &&
|
||||
this.created === other.created &&
|
||||
this.expires === other.expires
|
||||
);
|
||||
}
|
||||
|
||||
static sortFurthestExpiresFirst(a: Token, b: Token) {
|
||||
|
@ -1,5 +1,24 @@
|
||||
@use '../../../styles/theme.scss';
|
||||
|
||||
.date-spacer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 16px;
|
||||
|
||||
.horizontal-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: theme.$background-date-spacer;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
color: theme.$text-muted;
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-react {
|
||||
display: flex;
|
||||
padding: 4px 16px;
|
||||
|
@ -5,7 +5,12 @@ import { Member, Message } from '../../../data-types';
|
||||
import CombinedGuild from '../../../guild-combined';
|
||||
import ImageContextMenu from '../../contexts/context-menu-image';
|
||||
import ImageOverlay from '../../overlays/overlay-image';
|
||||
import { guildResourceSoftImgSrcState, guildResourceState, overlayState, useRecoilValueSoftImgSrc } from '../../require/atoms';
|
||||
import {
|
||||
guildResourceSoftImgSrcState,
|
||||
guildResourceState,
|
||||
overlayState,
|
||||
useRecoilValueSoftImgSrc,
|
||||
} from '../../require/atoms';
|
||||
import ElementsUtil, { IAlignment } from '../../require/elements-util';
|
||||
import { isLoaded } from '../../require/loadables';
|
||||
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';
|
||||
@ -19,14 +24,19 @@ interface ResourceElementProps {
|
||||
const ResourceElement: FC<ResourceElementProps> = (props: ResourceElementProps) => {
|
||||
const { guild, resourceId, resourceName } = props;
|
||||
|
||||
const [callable, text, shaking] = useDownloadButton(resourceName, async () => (await guild.fetchResource(resourceId)).data, [guild, resourceId], {
|
||||
start: 'Click to Download',
|
||||
pendingFetch: 'Fetching...',
|
||||
errorFetch: 'Unable to Download. Try Again',
|
||||
pendingSave: 'Saving...',
|
||||
errorSave: 'Unable to Save. Try Again',
|
||||
success: 'Click to Open in Explorer',
|
||||
});
|
||||
const [callable, text, shaking] = useDownloadButton(
|
||||
resourceName,
|
||||
async () => (await guild.fetchResource(resourceId)).data,
|
||||
[guild, resourceId],
|
||||
{
|
||||
start: 'Click to Download',
|
||||
pendingFetch: 'Fetching...',
|
||||
errorFetch: 'Unable to Download. Try Again',
|
||||
pendingSave: 'Saving...',
|
||||
errorSave: 'Unable to Save. Try Again',
|
||||
success: 'Click to Open in Explorer',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="content resource" onClick={callable}>
|
||||
@ -55,12 +65,22 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
|
||||
|
||||
// TODO: Handle resourceError
|
||||
const previewResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: resourcePreviewId }));
|
||||
const previewImgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: resourcePreviewId }));
|
||||
const previewImgSrc = useRecoilValueSoftImgSrc(
|
||||
guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: resourcePreviewId }),
|
||||
);
|
||||
|
||||
const [contextMenu, onContextMenu] = useContextClickContextMenu(
|
||||
(_alignment: IAlignment, relativeToPos: { x: number; y: number }, close: () => void) => {
|
||||
if (!isLoaded(previewResource)) return null;
|
||||
return <ImageContextMenu relativeToPos={relativeToPos} close={close} resourceName={resourceName} resourceBuff={previewResource.value.data} isPreview={true} />;
|
||||
return (
|
||||
<ImageContextMenu
|
||||
relativeToPos={relativeToPos}
|
||||
close={close}
|
||||
resourceName={resourceName}
|
||||
resourceBuff={previewResource.value.data}
|
||||
isPreview={true}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[previewResource, resourceName],
|
||||
);
|
||||
@ -86,7 +106,10 @@ export interface MessageElementProps {
|
||||
const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) => {
|
||||
const { guild, message, prevMessage } = props;
|
||||
|
||||
const className = useMemo(() => (message.isContinued(prevMessage) ? 'message-react continued' : 'message-react'), [message, prevMessage]);
|
||||
const className = useMemo(
|
||||
() => (message.isContinued(prevMessage) ? 'message-react continued' : 'message-react'),
|
||||
[message, prevMessage],
|
||||
);
|
||||
|
||||
const [avatarSrc] = useOneTimeAsyncAction(
|
||||
async () => {
|
||||
@ -98,6 +121,17 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
|
||||
[message],
|
||||
);
|
||||
|
||||
const dateSpacer = useMemo(() => {
|
||||
if (message.isContinued(prevMessage)) return null;
|
||||
return (
|
||||
<div className="date-spacer">
|
||||
<div className="horizontal-line" />
|
||||
<div className="date">{moment(message.sent).format('MMMM DD, YYYY')}</div>
|
||||
<div className="horizontal-line" />
|
||||
</div>
|
||||
);
|
||||
}, [message, prevMessage]);
|
||||
|
||||
const leftPart = useMemo(() => {
|
||||
if (message.isContinued(prevMessage)) {
|
||||
return <div className="timestamp">{moment(message.sent).format('HH:mm')}</div>;
|
||||
@ -152,19 +186,35 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ResourceElement guild={guild} resourceId={message.resourceId} resourceName={message.resourceName ?? 'unknown.unk'} />;
|
||||
return (
|
||||
<ResourceElement
|
||||
guild={guild}
|
||||
resourceId={message.resourceId}
|
||||
resourceName={message.resourceName ?? 'unknown.unk'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [guild, message.previewHeight, message.previewWidth, message.resourceId, message.resourceName, message.resourcePreviewId]);
|
||||
}, [
|
||||
guild,
|
||||
message.previewHeight,
|
||||
message.previewWidth,
|
||||
message.resourceId,
|
||||
message.resourceName,
|
||||
message.resourcePreviewId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{leftPart}
|
||||
<div className="right">
|
||||
{header}
|
||||
{resourceElement}
|
||||
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
|
||||
<>
|
||||
{dateSpacer}
|
||||
<div className={className}>
|
||||
{leftPart}
|
||||
<div className="right">
|
||||
{header}
|
||||
{resourceElement}
|
||||
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -50,7 +50,11 @@ export default class ElementsUtil {
|
||||
// calls a function with the start parameter and then the inverse of the start parameter after a determined number of ms
|
||||
// there is no way to cancel this function
|
||||
// useful for enabling a "shake" element for a pre-determined amount of time
|
||||
static async delayToggleState(setState: React.Dispatch<React.SetStateAction<boolean>>, delayMs: number, start = true): Promise<void> {
|
||||
static async delayToggleState(
|
||||
setState: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
delayMs: number,
|
||||
start = true,
|
||||
): Promise<void> {
|
||||
setState(start);
|
||||
await Util.sleep(delayMs);
|
||||
setState(_old => !start);
|
||||
@ -119,7 +123,11 @@ export default class ElementsUtil {
|
||||
const obj: SimpleQElement = { tag: 'span', content: [], class: null };
|
||||
const stack: SimpleQElement[] = [obj];
|
||||
let idx = 0;
|
||||
function makeEscape(regex: RegExp, len: number, str: string): { matcher: RegExp; response: (i: number) => void } {
|
||||
function makeEscape(
|
||||
regex: RegExp,
|
||||
len: number,
|
||||
str: string,
|
||||
): { matcher: RegExp; response: (i: number) => void } {
|
||||
// function for readability
|
||||
return {
|
||||
matcher: regex,
|
||||
@ -203,7 +211,11 @@ export default class ElementsUtil {
|
||||
// and align the left of the element to the right of the relative to plus 20 pixels
|
||||
// relativeTo can be an { x, y } or HTMLElement
|
||||
/** aligns an element relative to another element or a position in the window */
|
||||
static alignContextElement(element: HTMLElement, relativeTo: HTMLElement | { x: number; y: number }, alignment: IAlignment): void {
|
||||
static alignContextElement(
|
||||
element: HTMLElement,
|
||||
relativeTo: HTMLElement | { x: number; y: number },
|
||||
alignment: IAlignment,
|
||||
): void {
|
||||
function getOffset(alignment: string, name: string) {
|
||||
const added = name + ' +';
|
||||
const subbed = name + ' -';
|
||||
|
@ -39,6 +39,8 @@ $background-dropdown-option-hover: #1a1c1f;
|
||||
$channels-default: #8e9297;
|
||||
$channeltextarea-background: #40444b;
|
||||
|
||||
$background-date-spacer: #41454c;
|
||||
|
||||
$brand: #7289ba; /* yea that's a direct copy from discord */
|
||||
$brand-hover: #677bc4; /* thicc */
|
||||
$error: #d71f28; /* In fact, not copied! */
|
||||
|
Loading…
Reference in New Issue
Block a user