theme-fix+draftslist-fix+minieditor-cancel-fix
This commit is contained in:
parent
1a755f4c69
commit
0765c766b3
|
@ -42,12 +42,8 @@ bun run e2e:tests # Run tests
|
||||||
bun run e2e:tests:ci # Run tests in CI
|
bun run e2e:tests:ci # Run tests in CI
|
||||||
```
|
```
|
||||||
|
|
||||||
Structure:
|
|
||||||
- `/tests/*`: Tests without authentication
|
|
||||||
- `/tests-with-auth/*`: Tests with authentication
|
|
||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
|
|
||||||
Tests are executed in GitHub Actions. Make sure `BASE_URL` is correctly configured in CI.
|
Tests are executed in GitHub Actions. Make sure `BASE_URL` is correctly configured in CI.
|
||||||
|
|
||||||
## Project version: 0.9.7
|
## Version: 0.9.7
|
||||||
|
|
|
@ -43,12 +43,8 @@ bun run e2e:tests # Запуск тестов
|
||||||
bun run e2e:tests:ci # Запуск тестов в CI
|
bun run e2e:tests:ci # Запуск тестов в CI
|
||||||
```
|
```
|
||||||
|
|
||||||
Структура:
|
|
||||||
- `/tests/*`: Тесты без аутентификации
|
|
||||||
- `/tests-with-auth/*`: Тесты с аутентификацией
|
|
||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
|
|
||||||
Тесты выполняются в GitHub Actions. Убедитесь, что `BASE_URL` корректно настроен в CI.
|
Тесты выполняются в GitHub Actions. Убедитесь, что `BASE_URL` корректно настроен в CI.
|
||||||
|
|
||||||
## Версия проекта: 0.9.7
|
## Версия: 0.9.7
|
|
@ -53,7 +53,7 @@ export const Draft = (props: Props) => {
|
||||||
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}
|
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<A class={styles.actionItem} href={`edit/${props.shout?.id.toString()}`}>
|
<A class={styles.actionItem} href={`/edit/${props.shout?.id.toString()}`}>
|
||||||
{t('Edit')}
|
{t('Edit')}
|
||||||
</A>
|
</A>
|
||||||
<span onClick={handlePublishLinkClick} class={clsx(styles.actionItem, styles.publish)}>
|
<span onClick={handlePublishLinkClick} class={clsx(styles.actionItem, styles.publish)}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
import { UploadFile } from '@solid-primitives/upload'
|
import { UploadFile } from '@solid-primitives/upload'
|
||||||
import { Editor, EditorOptions } from '@tiptap/core'
|
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||||
|
@ -43,13 +43,8 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { session, requireAuthentication } = useSession()
|
const { session, requireAuthentication } = useSession()
|
||||||
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
|
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
|
||||||
const [isCommonMarkup, _setIsCommonMarkup] = createSignal(false)
|
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||||
const createMenuSignal = () => createSignal(false)
|
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
||||||
const [shouldShowTextBubbleMenu, _setShouldShowTextBubbleMenu] = createMenuSignal()
|
|
||||||
const [shouldShowBlockquoteBubbleMenu, _setShouldShowBlockquoteBubbleMenu] = createMenuSignal()
|
|
||||||
const [shouldShowFigureBubbleMenu, _setShouldShowFigureBubbleMenu] = createMenuSignal()
|
|
||||||
const [shouldShowIncutBubbleMenu, _setShouldShowIncutBubbleMenu] = createMenuSignal()
|
|
||||||
const [shouldShowFloatingMenu, _setShouldShowFloatingMenu] = createMenuSignal()
|
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { countWords, setEditing } = useEditorContext()
|
const { countWords, setEditing } = useEditorContext()
|
||||||
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
|
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
|
||||||
|
@ -180,14 +175,8 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
requireAuthentication(() => {
|
requireAuthentication(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setupEditor()
|
setupEditor()
|
||||||
|
|
||||||
// Создаем экземпляр редактора после монтирования
|
|
||||||
createEditorInstance(editorOptions())
|
createEditorInstance(editorOptions())
|
||||||
|
initializeMenus()
|
||||||
// Инициализируем меню после создания редактора
|
|
||||||
if (editor()) {
|
|
||||||
initializeMenus()
|
|
||||||
}
|
|
||||||
}, 1200)
|
}, 1200)
|
||||||
}, 'edit')
|
}, 'edit')
|
||||||
})
|
})
|
||||||
|
@ -196,30 +185,80 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
if (menusInitialized() || !editor()) return
|
if (menusInitialized() || !editor()) return
|
||||||
if (blockquoteBubbleMenuRef() && figureBubbleMenuRef() && incutBubbleMenuRef() && floatingMenuRef()) {
|
if (blockquoteBubbleMenuRef() && figureBubbleMenuRef() && incutBubbleMenuRef() && floatingMenuRef()) {
|
||||||
console.log('stage 3: initialize menus when editor instance is ready')
|
console.log('stage 3: initialize menus when editor instance is ready')
|
||||||
const menuConfigs = [
|
const menus = [
|
||||||
{ key: 'textBubbleMenu', ref: textBubbleMenuRef, shouldShow: shouldShowTextBubbleMenu },
|
BubbleMenu.configure({
|
||||||
{
|
pluginKey: 'textBubbleMenu',
|
||||||
key: 'blockquoteBubbleMenu',
|
element: textBubbleMenuRef()!,
|
||||||
ref: blockquoteBubbleMenuRef,
|
shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => {
|
||||||
shouldShow: shouldShowBlockquoteBubbleMenu
|
const isEmptyTextBlock =
|
||||||
},
|
doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
{ key: 'figureBubbleMenu', ref: figureBubbleMenuRef, shouldShow: shouldShowFigureBubbleMenu },
|
if (isEmptyTextBlock) {
|
||||||
{ key: 'incutBubbleMenu', ref: incutBubbleMenuRef, shouldShow: shouldShowIncutBubbleMenu },
|
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
|
||||||
{ key: 'floatingMenu', ref: floatingMenuRef, shouldShow: shouldShowFloatingMenu, isFloating: true }
|
}
|
||||||
|
const hasSelection = !selection.empty && from !== to
|
||||||
|
const isFootnoteOrFigcaption = e.isActive('footnote') || (e.isActive('figcaption') && hasSelection)
|
||||||
|
|
||||||
|
setIsCommonMarkup(e?.isActive('figcaption'))
|
||||||
|
|
||||||
|
const result = view.hasFocus() &&
|
||||||
|
hasSelection &&
|
||||||
|
!e.isActive('image') &&
|
||||||
|
!e.isActive('figure') &&
|
||||||
|
(isFootnoteOrFigcaption || !e.isActive('figcaption'))
|
||||||
|
|
||||||
|
setShouldShowTextBubbleMenu(result)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
tippyOptions: {
|
||||||
|
sticky: true,
|
||||||
|
// onHide: () => { editor()?.commands.focus() }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'blockquoteBubbleMenu',
|
||||||
|
element: blockquoteBubbleMenuRef()!,
|
||||||
|
shouldShow: ({ editor: e, state: { selection } }) => e.isFocused && !selection.empty && e.isActive('blockquote'),
|
||||||
|
tippyOptions: {
|
||||||
|
offset: [0, 0],
|
||||||
|
placement: 'top',
|
||||||
|
getReferenceClientRect: () => {
|
||||||
|
const selectedElement = editor()?.view.dom.querySelector('.has-focus')
|
||||||
|
return selectedElement?.getBoundingClientRect() || new DOMRect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'figureBubbleMenu',
|
||||||
|
element: figureBubbleMenuRef()!,
|
||||||
|
shouldShow: ({ editor: e, view }) => view.hasFocus() && e.isActive('figure')
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'incutBubbleMenu',
|
||||||
|
element: incutBubbleMenuRef()!,
|
||||||
|
shouldShow: ({ editor: e, state: { selection } }) => e.isFocused && !selection.empty && e.isActive('figcaption'),
|
||||||
|
tippyOptions: {
|
||||||
|
offset: [0, -16],
|
||||||
|
placement: 'top',
|
||||||
|
getReferenceClientRect: () => {
|
||||||
|
const selectedElement = editor()?.view.dom.querySelector('.has-focus')
|
||||||
|
return selectedElement?.getBoundingClientRect() || new DOMRect()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
FloatingMenu.configure({
|
||||||
|
element: floatingMenuRef()!,
|
||||||
|
pluginKey: 'floatingMenu',
|
||||||
|
shouldShow: ({ editor: e, state: { selection } }) => {
|
||||||
|
const { $anchor, empty } = selection
|
||||||
|
const isRootDepth = $anchor.depth === 1
|
||||||
|
if (!(isRootDepth && empty)) return false
|
||||||
|
return !(e.isActive('codeBlock') || e.isActive('heading'))
|
||||||
|
},
|
||||||
|
tippyOptions: {
|
||||||
|
placement: 'left',
|
||||||
|
}
|
||||||
|
})
|
||||||
]
|
]
|
||||||
const menus = menuConfigs.map((config) =>
|
|
||||||
config.isFloating
|
|
||||||
? FloatingMenu.configure({
|
|
||||||
pluginKey: config.key,
|
|
||||||
element: config.ref(),
|
|
||||||
shouldShow: config.shouldShow
|
|
||||||
})
|
|
||||||
: BubbleMenu.configure({
|
|
||||||
pluginKey: config.key,
|
|
||||||
element: config.ref(),
|
|
||||||
shouldShow: config.shouldShow
|
|
||||||
})
|
|
||||||
)
|
|
||||||
setEditorOptions((prev) => ({ ...prev, extensions: [...(prev.extensions || []), ...menus] }))
|
setEditorOptions((prev) => ({ ...prev, extensions: [...(prev.extensions || []), ...menus] }))
|
||||||
setMenusInitialized(true)
|
setMenusInitialized(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -311,12 +350,12 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
<Show when={editor()} keyed>
|
<Show when={editor()} keyed>
|
||||||
{(ed: Editor) => (
|
{(ed: Editor) => (
|
||||||
<>
|
<>
|
||||||
<TextBubbleMenu
|
<TextBubbleMenu
|
||||||
shouldShow={shouldShowTextBubbleMenu()}
|
shouldShow={shouldShowTextBubbleMenu()}
|
||||||
isCommonMarkup={isCommonMarkup()}
|
isCommonMarkup={isCommonMarkup()}
|
||||||
editor={ed}
|
editor={ed}
|
||||||
ref={setTextBubbleMenuRef}
|
ref={setTextBubbleMenuRef}
|
||||||
/>
|
/>
|
||||||
<BlockquoteBubbleMenu editor={ed} ref={setBlockquoteBubbleMenuRef} />
|
<BlockquoteBubbleMenu editor={ed} ref={setBlockquoteBubbleMenuRef} />
|
||||||
<FigureBubbleMenu editor={ed} ref={setFigureBubbleMenuRef} />
|
<FigureBubbleMenu editor={ed} ref={setFigureBubbleMenuRef} />
|
||||||
<IncutBubbleMenu editor={ed} ref={setIncutBubbleMenuRef} />
|
<IncutBubbleMenu editor={ed} ref={setIncutBubbleMenuRef} />
|
||||||
|
@ -338,4 +377,4 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -162,11 +162,14 @@ export function MiniEditor(props: MiniEditorProps): JSX.Element {
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
value={t('Cancel')}
|
value={t('Cancel')}
|
||||||
disabled={isEmpty()}
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => editor()?.commands.clearContent()}
|
onClick={() => {
|
||||||
|
editor()?.commands.clearContent()
|
||||||
|
props.onCancel?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button value={t('Send')} variant="primary" disabled={isEmpty()} onClick={handleSubmit} />
|
<Button value={t('Save')} variant="primary" disabled={isEmpty()} onClick={handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={counter() > 0}>
|
<Show when={counter() > 0}>
|
||||||
|
|
|
@ -16,11 +16,10 @@ import { Icon } from '~/components/_shared/Icon'
|
||||||
import { Popover } from '~/components/_shared/Popover'
|
import { Popover } from '~/components/_shared/Popover'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { InsertLinkForm } from './InsertLinkForm'
|
import { InsertLinkForm } from './InsertLinkForm'
|
||||||
|
import { MiniEditor } from '../MiniEditor'
|
||||||
|
|
||||||
import styles from './TextBubbleMenu.module.scss'
|
import styles from './TextBubbleMenu.module.scss'
|
||||||
|
|
||||||
const MiniEditor = lazy(() => import('../MiniEditor'))
|
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
type BubbleMenuProps = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
isCommonMarkup: boolean
|
isCommonMarkup: boolean
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
.name,
|
.name,
|
||||||
.message,
|
.message,
|
||||||
.time {
|
.time {
|
||||||
color: #fff !important;
|
color: var(--default-color-invert) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
8
src/components/_shared/LoadMoreWrapper.module.scss
Normal file
8
src/components/_shared/LoadMoreWrapper.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
.loadMoreWrapper {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.6em 1.5em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import { SortFunction } from '~/types/common'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
||||||
import { byCreated } from '~/utils/sort'
|
import { byCreated } from '~/utils/sort'
|
||||||
|
|
||||||
|
import styles from './LoadMoreWrapper.module.scss'
|
||||||
|
|
||||||
export type LoadMoreItems = Shout[] | Author[] | Reaction[]
|
export type LoadMoreItems = Shout[] | Author[] | Reaction[]
|
||||||
|
|
||||||
type LoadMoreProps = {
|
type LoadMoreProps = {
|
||||||
|
@ -58,7 +60,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
|
||||||
{props.children}
|
{props.children}
|
||||||
<div>
|
<div>
|
||||||
<Show when={isLoadMoreButtonVisible() && !props.hidden}>
|
<Show when={isLoadMoreButtonVisible() && !props.hidden}>
|
||||||
<div class="load-more-container">
|
<div class={styles.loadMoreWrapper}>
|
||||||
<Button
|
<Button
|
||||||
onClick={loadItems}
|
onClick={loadItems}
|
||||||
disabled={isLoading()}
|
disabled={isLoading()}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'theme';
|
||||||
|
|
||||||
$include-column-box-sizing: true !default;
|
$include-column-box-sizing: true !default;
|
||||||
$rfs-breakpoint: 1460px !default;
|
$rfs-breakpoint: 1460px !default;
|
||||||
$rfs-base-value: 1.6rem !default;
|
$rfs-base-value: 1.6rem !default;
|
||||||
|
@ -30,10 +32,10 @@ $font-family: muller, arial, helvetica, sans-serif;
|
||||||
$font-size-base: 2rem;
|
$font-size-base: 2rem;
|
||||||
$line-height-base: 1.6;
|
$line-height-base: 1.6;
|
||||||
|
|
||||||
// Базовые цвета (не зависящие от темы)
|
// цвета
|
||||||
$color-primary: #2638d9;
|
$color-primary: var(--blue-500);
|
||||||
$color-danger: #d00820;
|
$color-danger: var(--danger-color);
|
||||||
$default-color: #141414;
|
$default-color: var(--black-500);
|
||||||
|
|
||||||
// Другие переменные
|
// Другие переменные
|
||||||
$border-radius: 2px;
|
$border-radius: 2px;
|
||||||
|
|
|
@ -9,14 +9,15 @@
|
||||||
--secondary-color: #85878a;
|
--secondary-color: #85878a;
|
||||||
--placeholder-color: #9fa1a7;
|
--placeholder-color: #9fa1a7;
|
||||||
--placeholder-color-semi: rgb(159 169 167 / 20%);
|
--placeholder-color-semi: rgb(159 169 167 / 20%);
|
||||||
--danger-color: $color-danger;
|
--danger-color: #ff0000;
|
||||||
--lightgray-color: rgb(84 16 17 / 6%);
|
--lightgray-color: rgb(84 16 17 / 6%);
|
||||||
--selection-background: #000;
|
--selection-background: #000;
|
||||||
--selection-color: #fff;
|
--selection-color: #fff;
|
||||||
--icon-filter: invert(0);
|
--icon-filter: invert(0);
|
||||||
--icon-filter-hover: invert(1);
|
--icon-filter-hover: invert(1);
|
||||||
--editor-bubble-menu-background: #fff;
|
--editor-bubble-menu-background: #fff;
|
||||||
--blue-link: $link-color;
|
--editor-bubble-menu-border: #000;
|
||||||
|
--blue-link: #2638d9;
|
||||||
|
|
||||||
// names from figma
|
// names from figma
|
||||||
--black-50: #f7f7f8;
|
--black-50: #f7f7f8;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@import 'fonts';
|
@import 'fonts';
|
||||||
@import 'theme';
|
|
||||||
@import 'grid';
|
@import 'grid';
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
@ -603,8 +602,7 @@ figure {
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-switcher__counter {
|
.view-switcher__counter {
|
||||||
align-items: center;
|
background: var(--black-50);
|
||||||
background: #f7f7f8;
|
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
@ -838,14 +836,6 @@ figure {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more-container {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.6em 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
details {
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
padding-left: 3rem;
|
padding-left: 3rem;
|
||||||
|
@ -960,4 +950,32 @@ iframe {
|
||||||
|
|
||||||
.img-align-column {
|
.img-align-column {
|
||||||
clear: both;
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-switcher__counter {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--black-50);
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 2.2rem;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 2.2rem;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
min-width: 2.2rem;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.view-switcher__item--selected & {
|
||||||
|
background: var(--background-color-invert);
|
||||||
|
color: var(--default-color-invert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавим глобальный стиль для SVG
|
||||||
|
[data-theme='dark'] {
|
||||||
|
svg {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ main {
|
||||||
right: 0;
|
right: 0;
|
||||||
padding-left: 42px;
|
padding-left: 42px;
|
||||||
padding-right: 26px;
|
padding-right: 26px;
|
||||||
background: #fff;
|
background: var(--background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -65,7 +65,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list__search {
|
.chat-list__search {
|
||||||
border-bottom: 3px solid #141414;
|
border-bottom: 3px solid var(--default-color);
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user