Compare commits

..

3 Commits

Author SHA1 Message Date
CrescentLeaf
d4557ca0ae 乱了, 懒得说是什么 2026-01-02 01:27:32 +08:00
CrescentLeaf
bc603b8171 feat(WIP, unstable): 自定义输入框 2026-01-02 01:27:21 +08:00
CrescentLeaf
512419c131 fix: Cannot set properties of undefined (setting 'open') in opening ChatFragmentDialog 2026-01-01 20:52:17 +08:00
13 changed files with 123 additions and 22 deletions

13
client/env.d.ts vendored
View File

@@ -1,6 +1,19 @@
/// <reference types="mdui/jsx.zh-cn.d.ts" /> /// <reference types="mdui/jsx.zh-cn.d.ts" />
/// <reference types="vite/client" /> /// <reference types="vite/client" />
// 貌似没有起效
declare global {
namespace React {
namespace JSX {
interface IntrinsicElements {
'input-element': {
'value'?: string
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
}
}
}
}
declare const __APP_VERSION__: string declare const __APP_VERSION__: string
declare const __GIT_HASH__: string declare const __GIT_HASH__: string
declare const __GIT_HASH_FULL__: string declare const __GIT_HASH_FULL__: string

View File

@@ -14,6 +14,8 @@ import './ui/chat-elements/chat-text.ts'
import './ui/chat-elements/chat-mention.ts' import './ui/chat-elements/chat-mention.ts'
import './ui/chat-elements/chat-text-container.ts' import './ui/chat-elements/chat-text-container.ts'
import './ui/chat-elements/chat-quote.ts' import './ui/chat-elements/chat-quote.ts'
import './ui/InputElement.ts'
import './ui/InnerTextContainerElement.ts'
import Main from "./ui/Main.tsx" import Main from "./ui/Main.tsx"
import performAuth from './performAuth.ts' import performAuth from './performAuth.ts'

View File

@@ -0,0 +1,24 @@
export default class InnerTextContainerElement extends HTMLElement {
static observedAttributes = ['text']
declare textContainer: HTMLDivElement
declare slotContainer: HTMLSlotElement
declare text?: string
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot!.appendChild(document.createElement('slot'))
}
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (this.textContainer == null) {
this.textContainer = document.createElement('div')
// 注意这里不能加到 shadow
this.appendChild(this.textContainer)
this.textContainer.style.display = 'none'
}
this.textContainer.innerText = newValue || ''
}
}
customElements.define('inner-text-container', InnerTextContainerElement)

58
client/ui/InputElement.ts Normal file
View File

@@ -0,0 +1,58 @@
import { $ } from "mdui"
export default class InputElement extends HTMLElement {
static observedAttributes = ['user-id']
declare inputDiv: HTMLDivElement
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
_lastValue = ''
connectedCallback() {
const shadow = this.shadowRoot as ShadowRoot
this.inputDiv = new DOMParser().parseFromString(`
<div contentEditable="true" style="outline: none !important; color: rgb(var(--mdui-color-on-surface-variant)); display: inline-block; word-wrap: break-word; white-space: pre-wrap;"></div>
`, 'text/html').body.firstChild as HTMLDivElement
this.inputDiv.contentEditable = 'true'
this.inputDiv.addEventListener('blur', () => {
if (this._lastValue !== this.value) {
this._lastValue = this.value || ''
this.dispatchEvent(new Event('change', { bubbles: true }))
}
})
this.inputDiv.addEventListener('paste', (e: ClipboardEvent) => {
e.preventDefault()
document.execCommand('insertText', false, e.clipboardData?.getData("text/plain") || '')
})
this.inputDiv.style.width = '100%'
shadow.appendChild(this.inputDiv)
}
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
switch (name) {
case 'value': {
this.value = newValue || ''
break
}
}
}
focus() {
this.inputDiv.focus()
}
blur() {
this.inputDiv.blur()
}
get value() {
return this.inputDiv.textContent
}
set value(v) {
this.inputDiv.textContent = v
}
}
customElements.define('input-element', InputElement)

View File

@@ -7,7 +7,7 @@ type AppState = {
openUserInfo: (user: Chat | User | string) => void, openUserInfo: (user: Chat | User | string) => void,
openEditMyProfile: () => void, openEditMyProfile: () => void,
openAddFavouriteChat: () => void, openAddFavouriteChat: () => void,
openChat: (chat: string | Chat) => void, openChat: (chat: string | Chat, inDialog?: boolean) => void,
closeChat: () => void, closeChat: () => void,
} }

View File

@@ -12,6 +12,7 @@ import MainSharedContext, { Shared } from "../MainSharedContext"
import ChatFragmentDialog from "./ChatFragmentDialog" import ChatFragmentDialog from "./ChatFragmentDialog"
import useAsyncEffect from "../../utils/useAsyncEffect" import useAsyncEffect from "../../utils/useAsyncEffect"
import ClientCache from "../../ClientCache" import ClientCache from "../../ClientCache"
import isMobileUI from "../../utils/isMobileUI"
const config = await fetch('/config.json').then((re) => re.json()) const config = await fetch('/config.json').then((re) => re.json())
@@ -42,7 +43,6 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
MainSharedContext, MainSharedContext,
(context: Shared) => context.state.currentSelectedChatId (context: Shared) => context.state.currentSelectedChatId
) )
const [useChatFragmentDialog, setUseChatFragmentDialog] = React.useState(false)
const chatFragmentDialogRef = React.useRef<Dialog>() const chatFragmentDialogRef = React.useRef<Dialog>()
useAsyncEffect(async () => { useAsyncEffect(async () => {
@@ -70,10 +70,10 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
static async openChat(chat: string | Chat, inDialog?: boolean) { static async openChat(chat: string | Chat, inDialog?: boolean) {
if (chat instanceof Chat) chat = chat.getId() if (chat instanceof Chat) chat = chat.getId()
setUseChatFragmentDialog(inDialog || false)
setUserOrChatInfoDialogState([]) setUserOrChatInfoDialogState([])
setCurrentSelectedChatId(chat) setCurrentSelectedChatId(chat)
chatFragmentDialogRef.current!.open = true
inDialog && (chatFragmentDialogRef.current!.open = true)
} }
static closeChat() { static closeChat() {
if (chatFragmentDialogRef.current!.open) { if (chatFragmentDialogRef.current!.open) {
@@ -84,10 +84,10 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
setCurrentSelectedChatId('') setCurrentSelectedChatId('')
} }
}}> }}>
{<ChatFragmentDialog chatId={currentSelectedChatId} useRef={chatFragmentDialogRef} />}
<UserOrChatInfoDialog chat={userOrChatInfoDialogState[userOrChatInfoDialogState.length - 1] || lastUserOrChatInfoDialogStateRef.current} useRef={userOrChatInfoDialogRef} /> <UserOrChatInfoDialog chat={userOrChatInfoDialogState[userOrChatInfoDialogState.length - 1] || lastUserOrChatInfoDialogStateRef.current} useRef={userOrChatInfoDialogRef} />
<EditMyProfileDialog useRef={editMyProfileDialogRef} /> <EditMyProfileDialog useRef={editMyProfileDialogRef} />
<AddFavourtieChatDialog useRef={addFavouriteChatDialogRef} /> <AddFavourtieChatDialog useRef={addFavouriteChatDialogRef} />
{useChatFragmentDialog && currentSelectedChatId && currentSelectedChatId != '' && <ChatFragmentDialog chatId={currentSelectedChatId} useRef={chatFragmentDialogRef} />}
{children} {children}
</AppStateContext.Provider> </AppStateContext.Provider>
} }

View File

@@ -1,9 +1,10 @@
import { Dialog } from "mdui" import { Dialog } from "mdui"
import * as React from 'react' import * as React from 'react'
import LazyChatFragment from "../chat-fragment/LazyChatFragment" import LazyChatFragment from "../chat-fragment/LazyChatFragment"
import useEventListener from "../../utils/useEventListener"
export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string, useRef: React.MutableRefObject<Dialog | undefined> }) { export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string, useRef: React.MutableRefObject<Dialog | undefined> }) {
React.useEffect(() => { useEventListener(useRef, 'open', () => {
const shadow = useRef.current!.shadowRoot as ShadowRoot const shadow = useRef.current!.shadowRoot as ShadowRoot
const panel = shadow.querySelector(".panel") as HTMLElement const panel = shadow.querySelector(".panel") as HTMLElement
panel.style.padding = '0' panel.style.padding = '0'
@@ -13,14 +14,14 @@ export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string,
const body = shadow.querySelector(".body") as HTMLElement const body = shadow.querySelector(".body") as HTMLElement
body.style.height = '100%' body.style.height = '100%'
body.style.display = 'flex' body.style.display = 'flex'
}, [chatId]) })
return <mdui-dialog fullscreen ref={useRef}> return <mdui-dialog fullscreen ref={useRef}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
width: '100%', width: '100%',
}}> }}>
<LazyChatFragment chatId={chatId} openedInDialog={true} /> {chatId != null && chatId != '' && <LazyChatFragment chatId={chatId} openedInDialog={true} />}
</div> </div>
</mdui-dialog> </mdui-dialog>
} }

View File

@@ -110,7 +110,7 @@ export default function UserOrChatInfoDialog({ chat, useRef }: { chat?: Chat, us
})}>{favourited ? '取消收藏' : '收藏对话'}</mdui-list-item> })}>{favourited ? '取消收藏' : '收藏对话'}</mdui-list-item>
} }
<mdui-list-item icon="chat" rounded onClick={async () => { <mdui-list-item icon="chat" rounded onClick={async () => {
AppState.openChat(chat!) AppState.openChat(chat!, isMobileUI())
}}></mdui-list-item> }}></mdui-list-item>
</mdui-list> </mdui-list>
</mdui-dialog> </mdui-dialog>

View File

@@ -37,18 +37,14 @@ export default class ChatMentionElement extends HTMLElement {
const text = $(this).attr('text') const text = $(this).attr('text')
this.link.style.fontStyle = '' this.link.style.fontStyle = ''
if (chatId) { if (chatId) {
this.link.onclick = (e) => { this.link.onclick = (e) => {
e.stopPropagation() e.stopPropagation()
// deno-lint-ignore no-window this.openChatInfo?.(chatId)
} }
} else if (userId) { } else if (userId) {
this.link.onclick = (e) => { this.link.onclick = (e) => {
e.stopPropagation() e.stopPropagation()
// deno-lint-ignore no-window this.openUserInfo?.(userId)
} }
} }

View File

@@ -160,7 +160,7 @@ export default function ChatFragment({
}} onDrop={(e) => { }} onDrop={(e) => {
// 文件拽入 // 文件拽入
}}> }}>
<mdui-text-field variant="outlined" placeholder="(。・ω・。)" autosize ref={inputRef} max-rows={6} onChange={() => { <mdui-text-field variant="outlined" placeholder="(。・ω・。)" autosize ref={inputRef} /* max-rows={6} */ onChange={() => {
if (inputRef.current?.value.trim() == '') { if (inputRef.current?.value.trim() == '') {
// 清空缓存的文件 // 清空缓存的文件
} }
@@ -181,7 +181,12 @@ export default function ChatFragment({
marginRight: '10px', marginRight: '10px',
marginTop: '3px', marginTop: '3px',
marginBottom: '3px', marginBottom: '3px',
}}></mdui-text-field> }}>
{
// @ts-ignore
<input-element slot="input" />
}
</mdui-text-field>
<mdui-button-icon slot="end-icon" icon="attach_file" style={{ <mdui-button-icon slot="end-icon" icon="attach_file" style={{
marginRight: '6px', marginRight: '6px',
}} onClick={() => { }} onClick={() => {

View File

@@ -77,7 +77,7 @@ export default function RecentChatsList({ ...props }: React.HTMLAttributes<HTMLE
).map((v) => ).map((v) =>
<RecentsListItem <RecentsListItem
active={isMobileUI() ? false : shared.state.currentSelectedChatId == v.getId()} active={isMobileUI() ? false : shared.state.currentSelectedChatId == v.getId()}
onClick={() => AppState.openChat(v.getId())} onClick={() => AppState.openChat(v.getId(), isMobileUI())}
key={v.getId()} key={v.getId()}
recentChat={v} /> recentChat={v} />
) )

View File

@@ -1,5 +1,7 @@
import data from "../data" import data from "../data"
const searchParams = new URL(location.href).searchParams
export default function isMobileUI() { export default function isMobileUI() {
return data.override_use_mobile_ui || /Mobi|Android|iPhone/i.test(navigator.userAgent) return data.override_use_mobile_ui || searchParams.get('mobile') == 'true' || /Mobi|Android|iPhone/i.test(navigator.userAgent)
} }

View File

@@ -398,9 +398,9 @@ let TextField = class TextField extends FocusableMixin(MduiElement) {
'is-firefox': navigator.userAgent.includes('Firefox'), 'is-firefox': navigator.userAgent.includes('Firefox'),
...invalidClassNameObj, ...invalidClassNameObj,
}); });
return html `<div part="container" class="${className}">${this.renderPrefix()}<div class="input-container">${this.renderLabel()} ${this.isTextarea return html `<div part="container" class="${className}">${this.renderPrefix()}<div class="input-container">${this.renderLabel()} ${!hasInputSlot ? (this.isTextarea
? this.renderTextArea(hasInputSlot) ? this.renderTextArea(hasInputSlot)
: this.renderInput(hasInputSlot)} ${when(hasInputSlot, () => html `<slot name="input" class="input"></slot>`)}</div>${this.renderSuffix()}${this.renderClearButton(hasClearButton)} ${this.renderTogglePasswordButton(hasTogglePasswordButton)} ${this.renderRightIcon(hasErrorIcon)}</div>${when(hasError || hasHelper || hasCounter, () => html `<div part="supporting" class="${classMap({ supporting: true, ...invalidClassNameObj })}">${this.renderHelper(hasError, hasHelper)} ${this.renderCounter(hasCounter)}</div>`)}`; : this.renderInput(hasInputSlot)) : ''} ${when(hasInputSlot, () => html `<slot name="input" class="input"></slot>`)}</div>${this.renderSuffix()}${this.renderClearButton(hasClearButton)} ${this.renderTogglePasswordButton(hasTogglePasswordButton)} ${this.renderRightIcon(hasErrorIcon)}</div>${when(hasError || hasHelper || hasCounter, () => html `<div part="supporting" class="${classMap({ supporting: true, ...invalidClassNameObj })}">${this.renderHelper(hasError, hasHelper)} ${this.renderCounter(hasCounter)}</div>`)}`;
} }
setCustomValidityInternal(message) { setCustomValidityInternal(message) {
this.inputRef.value.setCustomValidity(message); this.inputRef.value.setCustomValidity(message);