Add access control rework
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/prefer-ngsrc -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<div #composerRoot>
|
||||
@if (replyTo()) {
|
||||
<div class="pointer-events-auto flex items-center gap-2 bg-secondary/50 px-4 py-2">
|
||||
|
||||
@@ -77,43 +77,17 @@
|
||||
@if (msg.isDeleted) {
|
||||
<div class="mt-1 text-sm italic text-muted-foreground">{{ deletedMessageContent }}</div>
|
||||
} @else {
|
||||
<div class="chat-markdown mt-1 break-words">
|
||||
<remark
|
||||
[markdown]="msg.content"
|
||||
[processor]="$any(remarkProcessor)"
|
||||
>
|
||||
<ng-template
|
||||
[remarkTemplate]="'code'"
|
||||
let-node
|
||||
>
|
||||
@if (isMermaidCodeBlock(node.lang) && getMermaidCode(node.value)) {
|
||||
<remark-mermaid [code]="getMermaidCode(node.value)" />
|
||||
} @else {
|
||||
<pre [class]="getCodeBlockClass(node.lang)"><code [class]="getCodeBlockClass(node.lang)">{{ node.value }}</code></pre>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template
|
||||
[remarkTemplate]="'image'"
|
||||
let-node
|
||||
>
|
||||
<div class="relative mt-2 inline-block overflow-hidden rounded-md border border-border/60 bg-secondary/20">
|
||||
<img
|
||||
[appChatImageProxyFallback]="node.url"
|
||||
[alt]="node.alt || 'Shared image'"
|
||||
class="block max-h-80 max-w-full w-auto"
|
||||
loading="lazy"
|
||||
/>
|
||||
@if (isKlipyMediaUrl(node.url)) {
|
||||
<span
|
||||
class="pointer-events-none absolute bottom-2 left-2 rounded-full bg-black/70 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.24em] text-white/90 backdrop-blur-sm"
|
||||
>
|
||||
KLIPY
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</remark>
|
||||
</div>
|
||||
@if (requiresRichMarkdown(msg.content)) {
|
||||
@defer {
|
||||
<div class="chat-markdown mt-1 break-words">
|
||||
<app-chat-message-markdown [content]="msg.content" />
|
||||
</div>
|
||||
} @placeholder {
|
||||
<div class="mt-1 whitespace-pre-wrap break-words text-sm text-foreground">{{ msg.content }}</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="mt-1 whitespace-pre-wrap break-words text-sm text-foreground">{{ msg.content }}</div>
|
||||
}
|
||||
|
||||
@if (attachmentsList.length > 0) {
|
||||
<div class="mt-2 space-y-2">
|
||||
|
||||
@@ -24,11 +24,6 @@ import {
|
||||
lucideTrash2,
|
||||
lucideX
|
||||
} from '@ng-icons/lucide';
|
||||
import { MermaidComponent, RemarkModule } from 'ngx-remark';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { unified } from 'unified';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentFacade,
|
||||
@@ -41,7 +36,7 @@ import {
|
||||
ChatVideoPlayerComponent,
|
||||
UserAvatarComponent
|
||||
} from '../../../../../../shared';
|
||||
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
|
||||
import { ChatMessageMarkdownComponent } from './chat-message-markdown.component';
|
||||
import {
|
||||
ChatMessageDeleteEvent,
|
||||
ChatMessageEditEvent,
|
||||
@@ -60,28 +55,7 @@ const COMMON_EMOJIS = [
|
||||
'🔥',
|
||||
'👀'
|
||||
];
|
||||
const PRISM_LANGUAGE_ALIASES: Record<string, string> = {
|
||||
cs: 'csharp',
|
||||
html: 'markup',
|
||||
js: 'javascript',
|
||||
md: 'markdown',
|
||||
plain: 'none',
|
||||
plaintext: 'none',
|
||||
py: 'python',
|
||||
sh: 'bash',
|
||||
shell: 'bash',
|
||||
svg: 'markup',
|
||||
text: 'none',
|
||||
ts: 'typescript',
|
||||
xml: 'markup',
|
||||
yml: 'yaml',
|
||||
zsh: 'bash'
|
||||
};
|
||||
const MERMAID_LINE_BREAK_PATTERN = /\r\n?/g;
|
||||
const REMARK_PROCESSOR = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkBreaks);
|
||||
const RICH_MARKDOWN_PATTERN = /(^|\n)(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|```|~~~)|!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)|`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_|(?:^|\n)\|.+\|/m;
|
||||
|
||||
interface ChatMessageAttachmentViewModel extends Attachment {
|
||||
isAudio: boolean;
|
||||
@@ -101,9 +75,7 @@ interface ChatMessageAttachmentViewModel extends Attachment {
|
||||
NgIcon,
|
||||
ChatAudioPlayerComponent,
|
||||
ChatVideoPlayerComponent,
|
||||
RemarkModule,
|
||||
MermaidComponent,
|
||||
ChatImageProxyFallbackDirective,
|
||||
ChatMessageMarkdownComponent,
|
||||
UserAvatarComponent
|
||||
],
|
||||
viewProviders: [
|
||||
@@ -136,7 +108,6 @@ export class ChatMessageItemComponent {
|
||||
readonly repliedMessage = input<Message | undefined>();
|
||||
readonly currentUserId = input<string | null>(null);
|
||||
readonly isAdmin = input(false);
|
||||
readonly remarkProcessor = REMARK_PROCESSOR;
|
||||
|
||||
readonly replyRequested = output<ChatMessageReplyEvent>();
|
||||
readonly deleteRequested = output<ChatMessageDeleteEvent>();
|
||||
@@ -320,23 +291,8 @@ export class ChatMessageItemComponent {
|
||||
);
|
||||
}
|
||||
|
||||
getMermaidCode(code?: string): string {
|
||||
return (code ?? '').replace(MERMAID_LINE_BREAK_PATTERN, '\n').trim();
|
||||
}
|
||||
|
||||
isKlipyMediaUrl(url?: string): boolean {
|
||||
if (!url)
|
||||
return false;
|
||||
|
||||
return /^(?:https?:)?\/\/(?:[^/]+\.)?klipy\.com/i.test(url);
|
||||
}
|
||||
|
||||
isMermaidCodeBlock(lang?: string): boolean {
|
||||
return this.normalizeCodeLanguage(lang) === 'mermaid';
|
||||
}
|
||||
|
||||
getCodeBlockClass(lang?: string): string {
|
||||
return `language-${this.normalizeCodeLanguage(lang)}`;
|
||||
requiresRichMarkdown(content: string): boolean {
|
||||
return RICH_MARKDOWN_PATTERN.test(content);
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
@@ -468,15 +424,6 @@ export class ChatMessageItemComponent {
|
||||
this.downloadRequested.emit(attachment);
|
||||
}
|
||||
|
||||
private normalizeCodeLanguage(lang?: string): string {
|
||||
const normalized = (lang || '').trim().toLowerCase();
|
||||
|
||||
if (!normalized)
|
||||
return 'none';
|
||||
|
||||
return PRISM_LANGUAGE_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
private buildAttachmentViewModel(attachment: Attachment): ChatMessageAttachmentViewModel {
|
||||
const isVideo = this.isVideoAttachment(attachment);
|
||||
const isAudio = this.isAudioAttachment(attachment);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<remark
|
||||
[markdown]="content()"
|
||||
[processor]="$any(remarkProcessor)"
|
||||
>
|
||||
<ng-template
|
||||
[remarkTemplate]="'code'"
|
||||
let-node
|
||||
>
|
||||
@if (isMermaidCodeBlock(node.lang) && getMermaidCode(node.value)) {
|
||||
<remark-mermaid [code]="getMermaidCode(node.value)" />
|
||||
} @else {
|
||||
<pre [class]="getCodeBlockClass(node.lang)"><code [class]="getCodeBlockClass(node.lang)">{{ node.value }}</code></pre>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template
|
||||
[remarkTemplate]="'image'"
|
||||
let-node
|
||||
>
|
||||
<div class="relative mt-2 inline-block overflow-hidden rounded-md border border-border/60 bg-secondary/20">
|
||||
<img
|
||||
[appChatImageProxyFallback]="node.url"
|
||||
[alt]="node.alt || 'Shared image'"
|
||||
class="block max-h-80 max-w-full w-auto"
|
||||
loading="lazy"
|
||||
/>
|
||||
@if (isKlipyMediaUrl(node.url)) {
|
||||
<span
|
||||
class="pointer-events-none absolute bottom-2 left-2 rounded-full bg-black/70 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.24em] text-white/90 backdrop-blur-sm"
|
||||
>
|
||||
KLIPY
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</remark>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { MermaidComponent, RemarkModule } from 'ngx-remark';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { unified } from 'unified';
|
||||
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
|
||||
|
||||
const PRISM_LANGUAGE_ALIASES: Record<string, string> = {
|
||||
cs: 'csharp',
|
||||
html: 'markup',
|
||||
js: 'javascript',
|
||||
md: 'markdown',
|
||||
plain: 'none',
|
||||
plaintext: 'none',
|
||||
py: 'python',
|
||||
sh: 'bash',
|
||||
shell: 'bash',
|
||||
svg: 'markup',
|
||||
text: 'none',
|
||||
ts: 'typescript',
|
||||
xml: 'markup',
|
||||
yml: 'yaml',
|
||||
zsh: 'bash'
|
||||
};
|
||||
const KLIPY_MEDIA_URL_PATTERN = /^(?:https?:)?\/\/(?:[^/]+\.)?klipy\.com/i;
|
||||
const MERMAID_LINE_BREAK_PATTERN = /\r\n?/g;
|
||||
const REMARK_PROCESSOR = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkBreaks);
|
||||
|
||||
@Component({
|
||||
selector: 'app-chat-message-markdown',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RemarkModule,
|
||||
MermaidComponent,
|
||||
ChatImageProxyFallbackDirective
|
||||
],
|
||||
templateUrl: './chat-message-markdown.component.html'
|
||||
})
|
||||
export class ChatMessageMarkdownComponent {
|
||||
readonly content = input.required<string>();
|
||||
readonly remarkProcessor = REMARK_PROCESSOR;
|
||||
|
||||
getMermaidCode(code?: string): string {
|
||||
return (code ?? '').replace(MERMAID_LINE_BREAK_PATTERN, '\n').trim();
|
||||
}
|
||||
|
||||
isKlipyMediaUrl(url?: string): boolean {
|
||||
if (!url)
|
||||
return false;
|
||||
|
||||
return KLIPY_MEDIA_URL_PATTERN.test(url);
|
||||
}
|
||||
|
||||
isMermaidCodeBlock(lang?: string): boolean {
|
||||
return this.normalizeCodeLanguage(lang) === 'mermaid';
|
||||
}
|
||||
|
||||
getCodeBlockClass(lang?: string): string {
|
||||
return `language-${this.normalizeCodeLanguage(lang)}`;
|
||||
}
|
||||
|
||||
private normalizeCodeLanguage(lang?: string): string {
|
||||
const normalized = (lang || '').trim().toLowerCase();
|
||||
|
||||
if (!normalized)
|
||||
return 'none';
|
||||
|
||||
return PRISM_LANGUAGE_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
|
||||
|
||||
readonly dateSeparatorLabels = computed(() => {
|
||||
const labels = new Map<number, string>();
|
||||
|
||||
let previousDayKey: string | null = null;
|
||||
|
||||
this.messages().forEach((message, index) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<!-- eslint-disable @angular-eslint/template/prefer-ngsrc -->
|
||||
<div
|
||||
class="flex h-[min(70vh,42rem)] w-full flex-col overflow-hidden rounded-[1.65rem] border border-border/80 shadow-2xl ring-1 ring-white/5"
|
||||
role="dialog"
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import {
|
||||
selectActiveChannelId,
|
||||
selectCurrentRoom
|
||||
} from '../../../../store/rooms/rooms.selectors';
|
||||
import { selectActiveChannelId, selectCurrentRoom } from '../../../../store/rooms/rooms.selectors';
|
||||
import {
|
||||
merge,
|
||||
interval,
|
||||
|
||||
Reference in New Issue
Block a user