Merge pull request #6949 from matrix-org/gsouquet/threads-relations
						commit
						2ad11b3af7
					
				| 
						 | 
				
			
			@ -271,9 +271,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
 | 
			
		|||
    componentDidMount() {
 | 
			
		||||
        this.calculateRoomMembersCount();
 | 
			
		||||
        this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
 | 
			
		||||
        if (SettingsStore.getValue("feature_thread")) {
 | 
			
		||||
            this.props.room?.getThreads().forEach(thread => thread.fetchReplyChain());
 | 
			
		||||
        }
 | 
			
		||||
        this.isMounted = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -463,8 +460,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
 | 
			
		|||
 | 
			
		||||
        // Checking if the message has a "parentEventId" as we do not
 | 
			
		||||
        // want to hide the root event of the thread
 | 
			
		||||
        if (mxEv.replyInThread && mxEv.parentEventId
 | 
			
		||||
                && this.props.hideThreadedMessages
 | 
			
		||||
        if (mxEv.isThreadRoot && this.props.hideThreadedMessages
 | 
			
		||||
                && SettingsStore.getValue("feature_thread")) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,7 +71,7 @@ const useFilteredThreadsTimelinePanel = ({
 | 
			
		|||
    userId,
 | 
			
		||||
    updateTimeline,
 | 
			
		||||
}: {
 | 
			
		||||
    threads: Set<Thread>;
 | 
			
		||||
    threads: Map<string, Thread>;
 | 
			
		||||
    room: Room;
 | 
			
		||||
    userId: string;
 | 
			
		||||
    filterOption: ThreadFilterType;
 | 
			
		||||
| 
						 | 
				
			
			@ -85,13 +85,13 @@ const useFilteredThreadsTimelinePanel = ({
 | 
			
		|||
    useEffect(() => {
 | 
			
		||||
        let filteredThreads = Array.from(threads);
 | 
			
		||||
        if (filterOption === ThreadFilterType.My) {
 | 
			
		||||
            filteredThreads = filteredThreads.filter(thread => {
 | 
			
		||||
            filteredThreads = filteredThreads.filter(([id, thread]) => {
 | 
			
		||||
                return thread.rootEvent.getSender() === userId;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        // NOTE: Temporarily reverse the list until https://github.com/vector-im/element-web/issues/19393 gets properly resolved
 | 
			
		||||
        // The proper list order should be top-to-bottom, like in social-media newsfeeds.
 | 
			
		||||
        filteredThreads.reverse().forEach(thread => {
 | 
			
		||||
        filteredThreads.reverse().forEach(([id, thread]) => {
 | 
			
		||||
            const event = thread.rootEvent;
 | 
			
		||||
            if (timelineSet.findEventById(event.getId()) || event.status !== null) return;
 | 
			
		||||
            timelineSet.addEventToTimeline(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ limitations under the License.
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
 | 
			
		||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
 | 
			
		||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
 | 
			
		||||
 | 
			
		||||
import BaseCard from "../views/right_panel/BaseCard";
 | 
			
		||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
 | 
			
		||||
| 
						 | 
				
			
			@ -185,8 +186,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
 | 
			
		|||
                    { this.state?.thread?.timelineSet && (<MessageComposer
 | 
			
		||||
                        room={this.props.room}
 | 
			
		||||
                        resizeNotifier={this.props.resizeNotifier}
 | 
			
		||||
                        replyInThread={true}
 | 
			
		||||
                        replyToEvent={this.state?.thread?.replyToEvent}
 | 
			
		||||
                        relation={{
 | 
			
		||||
                            rel_type: RelationType.Thread,
 | 
			
		||||
                            event_id: this.state.thread.id,
 | 
			
		||||
                        }}
 | 
			
		||||
                        showReplyPreview={false}
 | 
			
		||||
                        permalinkCreator={this.props.permalinkCreator}
 | 
			
		||||
                        e2eStatus={this.props.e2eStatus}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,6 @@ import classNames from 'classnames';
 | 
			
		|||
import { _t } from '../../../languageHandler';
 | 
			
		||||
import dis from '../../../dispatcher/dispatcher';
 | 
			
		||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 | 
			
		||||
import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
 | 
			
		||||
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
 | 
			
		||||
import SettingsStore from "../../../settings/SettingsStore";
 | 
			
		||||
import { Layout } from "../../../settings/Layout";
 | 
			
		||||
| 
						 | 
				
			
			@ -225,28 +224,15 @@ export default class ReplyThread extends React.Component<IProps, IState> {
 | 
			
		|||
        return { body, html };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
 | 
			
		||||
    public static makeReplyMixIn(ev: MatrixEvent) {
 | 
			
		||||
        if (!ev) return {};
 | 
			
		||||
 | 
			
		||||
        const replyMixin = {
 | 
			
		||||
        return {
 | 
			
		||||
            'm.relates_to': {
 | 
			
		||||
                'm.in_reply_to': {
 | 
			
		||||
                    'event_id': ev.getId(),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * @experimental
 | 
			
		||||
         * Rendering hint for threads, only attached if true to make
 | 
			
		||||
         * sure that Element does not start sending that property for all events
 | 
			
		||||
         */
 | 
			
		||||
        if (replyInThread) {
 | 
			
		||||
            const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
 | 
			
		||||
            inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return replyMixin;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static hasThreadReply(event: MatrixEvent) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
 | 
			
		|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
 | 
			
		||||
import SendHistoryManager from '../../../SendHistoryManager';
 | 
			
		||||
import Modal from '../../../Modal';
 | 
			
		||||
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } from 'matrix-js-sdk/src/@types/event';
 | 
			
		||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
 | 
			
		||||
import { Room } from 'matrix-js-sdk/src/models/room';
 | 
			
		||||
import ErrorDialog from "../dialogs/ErrorDialog";
 | 
			
		||||
import QuestionDialog from "../dialogs/QuestionDialog";
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 | 
			
		|||
 | 
			
		||||
import { logger } from "matrix-js-sdk/src/logger";
 | 
			
		||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
 | 
			
		||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
 | 
			
		||||
import RoomContext from '../../../contexts/RoomContext';
 | 
			
		||||
 | 
			
		||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
 | 
			
		||||
    const html = mxEvent.getContent().formatted_body;
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +70,6 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
 | 
			
		|||
function createEditContent(
 | 
			
		||||
    model: EditorModel,
 | 
			
		||||
    editedEvent: MatrixEvent,
 | 
			
		||||
    renderingContext?: TimelineRenderingType,
 | 
			
		||||
): IContent {
 | 
			
		||||
    const isEmote = containsEmote(model);
 | 
			
		||||
    if (isEmote) {
 | 
			
		||||
| 
						 | 
				
			
			@ -112,10 +111,6 @@ function createEditContent(
 | 
			
		|||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (renderingContext === TimelineRenderingType.Thread) {
 | 
			
		||||
        relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Object.assign(relation, contentBody);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,8 +138,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
 | 
			
		|||
        const isRestored = this.createEditorModel();
 | 
			
		||||
        const ev = this.props.editState.getEvent();
 | 
			
		||||
 | 
			
		||||
        const renderingContext = this.context.timelineRenderingType;
 | 
			
		||||
        const editContent = createEditContent(this.model, ev, renderingContext);
 | 
			
		||||
        const editContent = createEditContent(this.model, ev);
 | 
			
		||||
        this.state = {
 | 
			
		||||
            saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
 | 
			
		||||
        };
 | 
			
		||||
| 
						 | 
				
			
			@ -369,8 +363,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
 | 
			
		|||
            const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
 | 
			
		||||
            this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
 | 
			
		||||
        }
 | 
			
		||||
        const renderingContext = this.context.timelineRenderingType;
 | 
			
		||||
        const editContent = createEditContent(this.model, editedEvent, renderingContext);
 | 
			
		||||
        const editContent = createEditContent(this.model, editedEvent);
 | 
			
		||||
        const newContent = editContent["m.new_content"];
 | 
			
		||||
 | 
			
		||||
        let shouldSend = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ import React, { createRef } from 'react';
 | 
			
		|||
import classNames from 'classnames';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
 | 
			
		||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 | 
			
		||||
import { MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
 | 
			
		||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 | 
			
		||||
import dis from '../../../dispatcher/dispatcher';
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +54,7 @@ import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
 | 
			
		|||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
 | 
			
		||||
import Modal from "../../../Modal";
 | 
			
		||||
import InfoDialog from "../dialogs/InfoDialog";
 | 
			
		||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
 | 
			
		||||
 | 
			
		||||
let instanceCount = 0;
 | 
			
		||||
const NARROW_MODE_BREAKPOINT = 500;
 | 
			
		||||
| 
						 | 
				
			
			@ -225,7 +226,7 @@ interface IProps {
 | 
			
		|||
    resizeNotifier: ResizeNotifier;
 | 
			
		||||
    permalinkCreator: RoomPermalinkCreator;
 | 
			
		||||
    replyToEvent?: MatrixEvent;
 | 
			
		||||
    replyInThread?: boolean;
 | 
			
		||||
    relation?: IEventRelation;
 | 
			
		||||
    showReplyPreview?: boolean;
 | 
			
		||||
    e2eStatus?: E2EStatus;
 | 
			
		||||
    compact?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -252,7 +253,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
 | 
			
		|||
    private instanceId: number;
 | 
			
		||||
 | 
			
		||||
    static defaultProps = {
 | 
			
		||||
        replyInThread: false,
 | 
			
		||||
        showReplyPreview: true,
 | 
			
		||||
        compact: false,
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -378,9 +378,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
 | 
			
		|||
 | 
			
		||||
    private renderPlaceholderText = () => {
 | 
			
		||||
        if (this.props.replyToEvent) {
 | 
			
		||||
            if (this.props.replyInThread && this.props.e2eStatus) {
 | 
			
		||||
            const replyingToThread = this.props.relation?.rel_type === RelationType.Thread;
 | 
			
		||||
            if (replyingToThread && this.props.e2eStatus) {
 | 
			
		||||
                return _t('Reply to encrypted thread…');
 | 
			
		||||
            } else if (this.props.replyInThread) {
 | 
			
		||||
            } else if (replyingToThread) {
 | 
			
		||||
                return _t('Reply to thread…');
 | 
			
		||||
            } else if (this.props.e2eStatus) {
 | 
			
		||||
                return _t('Send an encrypted reply…');
 | 
			
		||||
| 
						 | 
				
			
			@ -558,7 +559,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
 | 
			
		|||
                    room={this.props.room}
 | 
			
		||||
                    placeholder={this.renderPlaceholderText()}
 | 
			
		||||
                    permalinkCreator={this.props.permalinkCreator}
 | 
			
		||||
                    replyInThread={this.props.replyInThread}
 | 
			
		||||
                    relation={this.props.relation}
 | 
			
		||||
                    replyToEvent={this.props.replyToEvent}
 | 
			
		||||
                    onChange={this.onChange}
 | 
			
		||||
                    disabled={this.state.haveRecording}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react';
 | 
			
		||||
import EMOJI_REGEX from 'emojibase-regex';
 | 
			
		||||
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
 | 
			
		||||
import { IContent, MatrixEvent, IEventRelation } from 'matrix-js-sdk/src/models/event';
 | 
			
		||||
import { DebouncedFunc, throttle } from 'lodash';
 | 
			
		||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
 | 
			
		||||
import { logger } from "matrix-js-sdk/src/logger";
 | 
			
		||||
| 
						 | 
				
			
			@ -61,10 +61,10 @@ import RoomContext from '../../../contexts/RoomContext';
 | 
			
		|||
function addReplyToMessageContent(
 | 
			
		||||
    content: IContent,
 | 
			
		||||
    replyToEvent: MatrixEvent,
 | 
			
		||||
    replyInThread: boolean,
 | 
			
		||||
    permalinkCreator: RoomPermalinkCreator,
 | 
			
		||||
    relation?: IEventRelation,
 | 
			
		||||
): void {
 | 
			
		||||
    const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread);
 | 
			
		||||
    const replyContent = ReplyThread.makeReplyMixIn(replyToEvent);
 | 
			
		||||
    Object.assign(content, replyContent);
 | 
			
		||||
 | 
			
		||||
    // Part of Replies fallback support - prepend the text we're sending
 | 
			
		||||
| 
						 | 
				
			
			@ -76,13 +76,20 @@ function addReplyToMessageContent(
 | 
			
		|||
        }
 | 
			
		||||
        content.body = nestedReply.body + content.body;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (relation) {
 | 
			
		||||
        content['m.relates_to'] = {
 | 
			
		||||
            ...relation, // the composer can have a default
 | 
			
		||||
            ...content['m.relates_to'],
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// exported for tests
 | 
			
		||||
export function createMessageContent(
 | 
			
		||||
    model: EditorModel,
 | 
			
		||||
    replyToEvent: MatrixEvent,
 | 
			
		||||
    replyInThread: boolean,
 | 
			
		||||
    relation: IEventRelation,
 | 
			
		||||
    permalinkCreator: RoomPermalinkCreator,
 | 
			
		||||
): IContent {
 | 
			
		||||
    const isEmote = containsEmote(model);
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +113,14 @@ export function createMessageContent(
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (replyToEvent) {
 | 
			
		||||
        addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator);
 | 
			
		||||
        addReplyToMessageContent(content, replyToEvent, permalinkCreator);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (relation) {
 | 
			
		||||
        content['m.relates_to'] = {
 | 
			
		||||
            ...relation,
 | 
			
		||||
            ...content['m.relates_to'],
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return content;
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +148,7 @@ interface ISendMessageComposerProps extends MatrixClientProps {
 | 
			
		|||
    room: Room;
 | 
			
		||||
    placeholder?: string;
 | 
			
		||||
    permalinkCreator: RoomPermalinkCreator;
 | 
			
		||||
    replyInThread?: boolean;
 | 
			
		||||
    relation?: IEventRelation;
 | 
			
		||||
    replyToEvent?: MatrixEvent;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    onChange?(model: EditorModel): void;
 | 
			
		||||
| 
						 | 
				
			
			@ -164,12 +178,11 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
 | 
			
		||||
        const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
 | 
			
		||||
        if (replyToEventChanged) {
 | 
			
		||||
            this.model.reset([]);
 | 
			
		||||
        }
 | 
			
		||||
        const replyingToThread = this.props.relation?.key === RelationType.Thread;
 | 
			
		||||
        const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;
 | 
			
		||||
 | 
			
		||||
        if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
 | 
			
		||||
        const threadChanged = replyingToThread && (differentEventTarget);
 | 
			
		||||
        if (threadChanged) {
 | 
			
		||||
            const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
 | 
			
		||||
            const parts = this.restoreStoredEditorState(partCreator) || [];
 | 
			
		||||
            this.model.reset(parts);
 | 
			
		||||
| 
						 | 
				
			
			@ -182,6 +195,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
 | 
			
		|||
        if (this.editorRef.current?.isComposing(event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const replyingToThread = this.props.relation?.key === RelationType.Thread;
 | 
			
		||||
        const action = getKeyBindingsManager().getMessageComposerAction(event);
 | 
			
		||||
        switch (action) {
 | 
			
		||||
            case MessageComposerAction.Send:
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +217,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
 | 
			
		|||
                if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
 | 
			
		||||
                    const events =
 | 
			
		||||
                        this.context.liveTimeline.getEvents()
 | 
			
		||||
                            .concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents());
 | 
			
		||||
                            .concat(replyingToThread ? [] : this.props.room.getPendingEvents());
 | 
			
		||||
                    const editEvent = findEditableEvent({
 | 
			
		||||
                        events,
 | 
			
		||||
                        isForward: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -395,8 +409,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
 | 
			
		|||
                        addReplyToMessageContent(
 | 
			
		||||
                            content,
 | 
			
		||||
                            replyToEvent,
 | 
			
		||||
                            this.props.replyInThread,
 | 
			
		||||
                            this.props.permalinkCreator,
 | 
			
		||||
                            this.props.relation,
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -443,7 +457,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
 | 
			
		|||
                content = createMessageContent(
 | 
			
		||||
                    model,
 | 
			
		||||
                    replyToEvent,
 | 
			
		||||
                    this.props.replyInThread,
 | 
			
		||||
                    this.props.relation,
 | 
			
		||||
                    this.props.permalinkCreator,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -517,7 +531,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    private restoreStoredEditorState(partCreator: PartCreator): Part[] {
 | 
			
		||||
        if (this.props.replyInThread && !this.props.replyToEvent) {
 | 
			
		||||
        const replyingToThread = this.props.relation?.key === RelationType.Thread;
 | 
			
		||||
        if (replyingToThread) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue