pixelfed/pixelfed

View on GitHub
resources/assets/js/components/DirectMessage.vue

Summary

Maintainability
Test Coverage
<template>
<div>
    <div v-if="loaded && page == 'read'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
        <div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
            <div class="card shadow-none border mt-4">
                <div class="card-header bg-white d-flex justify-content-between align-items-center">
                    <span>
                        <a href="/account/direct" class="text-muted">
                            <i class="fas fa-chevron-left fa-lg"></i>
                        </a>
                    </span>
                    <span>
                        <div class="media">
                            <img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="40" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
                            <div class="media-body">
                                <p class="mb-0">
                                    <span class="font-weight-bold">{{thread.name}}</span>
                                </p>
                                <p class="mb-0">
                                    <a v-if="!thread.isLocal" :href="'/'+thread.username" class="text-decoration-none text-muted">{{thread.username}}</a>
                                    <a v-else :href="'/'+thread.username" class="text-decoration-none text-muted">&commat;{{thread.username}}</a>
                                </p>
                            </div>
                        </div>   
                    </span>
                    <span><a href="#" class="text-muted" @click.prevent="showOptions()"><i class="fas fa-cog fa-lg"></i></a></span>
                </div>
                <ul class="list-group list-group-flush dm-wrapper" style="height:60vh;overflow-y: scroll;">
                    <li class="list-group-item border-0">
                        <p class="text-center small text-muted">
                            Conversation with <span class="font-weight-bold">{{thread.username}}</span>
                        </p>
                        <hr>
                    </li>
                    <li v-if="showLoadMore && thread.messages && thread.messages.length > 5" class="list-group-item border-0 mt-n4">
                        <p class="text-center small text-muted">
                            <button v-if="!loadingMessages" class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" @click="loadOlderMessages()">Load Older Messages</button>
                            <button v-else class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" disabled>Loading...</button>
                        </p>
                    </li> 
                    <li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)">
                        <div v-if="!convo.isAuthor" class="media d-inline-flex mb-0">
                            <img v-if="!hideAvatars" class="mr-3 mt-2 rounded-circle img-thumbnail" :src="thread.avatar" alt="avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
                            <div class="media-body">
                                <p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
                                    <img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
                                </p>
                                <div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
                                    <div class="media-body">
                                        <div class="card mb-2 rounded border shadow" style="width:240px;" :title="convo.text">
                                            <div class="card-body p-0">
                                                <div class="media d-flex align-items-center">
                                                    <div v-if="convo.meta.local" class="bg-primary mr-3 border-right p-3">
                                                        <i class="fas fa-link text-white fa-2x"></i>
                                                    </div>
                                                    <div v-else class="bg-light mr-3 border-right p-3">
                                                        <i class="fas fa-link text-lighter fa-2x"></i>
                                                    </div>
                                                    <div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
                                                        {{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <p v-else-if="convo.type == 'video'" class="pill-to p-0 shadow">
                                    <!-- <video :src="convo.media" width="140px" style="border-radius:20px;"></video> -->
                                    <span class="d-block bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px;border-radius: 20px;">
                                        <div class="text-center">
                                            <p class="mb-1">
                                                <i class="fas fa-play fa-2x text-white"></i>
                                            </p>
                                            <p class="mb-0 small font-weight-bold text-white">
                                                Play
                                            </p>
                                        </div>
                                    </span>
                                </p>
                                <p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
                                    {{convo.text}}
                                </p>
                                <p v-else-if="convo.type == 'story:react'" class="pill-to p-0 shadow" style="width: 140px;margin-bottom: 10px;position:relative;">
                                    <img :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
                                    <span class="badge badge-light rounded-pill border" style="font-size: 20px;position: absolute;bottom:-10px;left:-10px;">
                                        {{convo.meta.reaction}}
                                    </span>
                                </p>
                                <span v-else-if="convo.type == 'story:comment'" class="p-0" style="display: flex;justify-content: flex-start;margin-bottom: 10px;position:relative;">
                                    <span class="">
                                        <img class="d-block pill-to p-0 mr-0 pr-0 mb-n1" :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
                                        <span class="pill-to shadow text-break" style="width:fit-content;">{{convo.meta.caption}}</span>
                                    </span>
                                </span>
                                <p v-else :class="[largerText ? 'pill-to shadow larger-text text-break':'pill-to shadow text-break']">
                                    {{convo.text}}
                                </p>
                                <p v-if="convo.type == 'story:react'" class="small text-muted mb-0 ml-0">
                                    <span class="font-weight-bold">{{ convo.meta.story_actor_username }}</span> reacted your story
                                </p>
                                <p v-if="convo.type == 'story:comment'" class="small text-muted mb-0 ml-0">
                                    <span class="font-weight-bold">{{ convo.meta.story_actor_username }}</span> replied to your story
                                </p>
                                <p v-if="!hideTimestamps" class="small text-muted font-weight-bold d-flex align-items-center justify-content-start" data-timestamp="timestamp"> <span v-if="convo.hidden" class="mr-2 small" title="Filtered Message" data-toggle="tooltip" data-placement="bottom"><i class="fas fa-lock"></i></span> {{convo.timeAgo}}</p>
                                <p v-else>&nbsp;</p>
                            </div>
                        </div>
                        <div v-else class="media d-inline-flex float-right mb-0 mr-2">
                            <div class="media-body">
                                <p v-if="convo.type == 'photo'" class="pill-from p-0 shadow">
                                    <img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
                                </p>
                                <div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
                                    <div class="media-body">
                                        <div class="card mb-2 rounded border shadow" style="width:240px;" :title="convo.text">
                                            <div class="card-body p-0">
                                                <div class="media d-flex align-items-center">
                                                    <div v-if="convo.meta.local" class="bg-primary mr-3 border-right p-3">
                                                        <i class="fas fa-link text-white fa-2x"></i>
                                                    </div>
                                                    <div v-else class="bg-light mr-3 border-right p-3">
                                                        <i class="fas fa-link text-lighter fa-2x"></i>
                                                    </div>
                                                    <div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
                                                        {{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <p v-else-if="convo.type == 'video'" class="pill-from p-0 shadow">
                                    <!-- <video :src="convo.media" width="140px" style="border-radius:20px;"></video> -->
                                    <span class="rounded-pill bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px">
                                        <div class="text-center">
                                            <p class="mb-1">
                                                <i class="fas fa-play fa-2x text-white"></i>
                                            </p>
                                            <p class="mb-0 small font-weight-bold">
                                                Play
                                            </p>
                                        </div>
                                    </span>
                                </p>
                                <p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
                                    {{convo.text}}
                                </p>
                                <p v-else-if="convo.type == 'story:react'" class="pill-from p-0 shadow" style="margin-bottom: 10px;position:relative;width:fit-content;">
                                    <img :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
                                    <span class="badge badge-light rounded-pill border" style="font-size: 20px;position: absolute;bottom:-10px;right:-10px;">
                                        {{convo.meta.reaction}}
                                    </span>
                                </p>
                                <span v-else-if="convo.type == 'story:comment'" class="p-0" style="display: flex;justify-content: flex-end;margin-bottom: 10px;position:relative;">
                                    <span class="d-flex align-items-end flex-column">
                                        <img class="d-block pill-from p-0 mr-0 pr-0 mb-n1" :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
                                        <span class="pill-from shadow text-break" style="width:fit-content;">{{convo.meta.caption}}</span>
                                    </span>
                                </span>
                                <p v-else :class="[largerText ? 'pill-from shadow larger-text text-break':'pill-from shadow text-break']">
                                    {{convo.text}}
                                </p>
                                <p v-if="convo.type == 'story:react'" class="small text-muted text-right mb-0 mr-0">
                                    You reacted to <span class="font-weight-bold">{{ convo.meta.story_username }}</span>'s story
                                </p>
                                <p v-if="convo.type == 'story:comment'" class="small text-muted text-right mb-0 mr-0">
                                    You replied to <span class="font-weight-bold">{{ convo.meta.story_username }}</span>'s story
                                </p>
                                <p v-if="!hideTimestamps" class="small text-muted font-weight-bold text-right"> <span v-if="convo.hidden" class="mr-2 small" title="Filtered Message" data-toggle="tooltip" data-placement="bottom"><i class="fas fa-lock"></i></span> {{convo.timeAgo}}
                                </p>
                                <p v-else>&nbsp;</p>
                            </div>
                            <img v-if="!hideAvatars" class="ml-3 mt-2 rounded-circle img-thumbnail" :src="profile.avatar" alt="avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
                        </div>
                    </li>

                </ul>
                <div class="card-footer bg-white p-0">
                    <form class="border-0 rounded-0 align-middle" method="post" action="#">
                        <textarea class="form-control border-0 rounded-0 no-focus" name="comment" placeholder="Reply ..." autocomplete="off" autocorrect="off" style="height:86px;line-height: 18px;max-height:80px;resize: none; padding-right:115.22px;" v-model="replyText" :disabled="blocked"></textarea>
                        <input type="button" value="Send" :class="[replyText.length ? 'd-inline-block btn btn-sm btn-primary rounded-pill font-weight-bold reply-btn text-decoration-none text-uppercase' : 'd-inline-block btn btn-sm btn-primary rounded-pill font-weight-bold reply-btn text-decoration-none text-uppercase disabled']" :disabled="replyText.length == 0" @click.prevent="sendMessage"/>
                    </form>
                </div>
                <div class="card-footer p-0">
                    <p class="d-flex justify-content-between align-items-center mb-0 px-3 py-1 small">
                        <!-- <span class="font-weight-bold" style="color: #D69E2E">
                        <i class="fas fa-circle mr-1"></i>
                        Typing ...
                        </span> -->
                        <span>
                            <!-- <span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
                            <i class="fas fa-share mr-1"></i>
                            Share
                            </span> -->
                            <span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
                                <i class="fas fa-upload mr-1"></i>
                                Add Photo/Video
                            </span>
                        </span>
                        <input type="file" id="uploadMedia" class="d-none" name="uploadMedia" accept="image/jpeg,image/png,image/gif,video/mp4" >
                        <span class="text-muted font-weight-bold">{{replyText.length}}/600</span>
                    </p>
                </div>
            </div>
        </div>
    </div>
    <div v-if="loaded && page == 'options'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
        <div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
            <div class="card shadow-none border mt-4">
                <div class="card-header bg-white d-flex justify-content-between align-items-center">
                    <span>
                        <a href="#" class="text-muted" @click.prevent="page='read'">
                            <i class="fas fa-chevron-left fa-lg"></i>
                        </a>
                    </span>
                    <span>
                        <p class="mb-0 lead font-weight-bold py-2">Message Settings</p>
                    </span>
                    <span class="text-lighter" data-toggle="tooltip" data-placement="bottom" title="Have a nice day!"><i class="far fa-smile fa-lg"></i></span>
                </div>
                <ul class="list-group list-group-flush dm-wrapper" style="height: 698px;">
                    <div class="list-group-item media border-bottom">
                        <div class="d-inline-block custom-control custom-switch ml-3">
                            <input type="checkbox" class="custom-control-input" id="customSwitch0" v-model="hideAvatars">
                            <label class="custom-control-label" for="customSwitch0"></label>
                        </div>
                        <div class="d-inline-block ml-3 font-weight-bold">
                            Hide Avatars
                        </div>
                    </div>
                    <div class="list-group-item media border-bottom">
                        <div class="d-inline-block custom-control custom-switch ml-3">
                            <input type="checkbox" class="custom-control-input" id="customSwitch1" v-model="hideTimestamps">
                            <label class="custom-control-label" for="customSwitch1"></label>
                        </div>
                        <div class="d-inline-block ml-3 font-weight-bold">
                            Hide Timestamps
                        </div>
                    </div>
                    <div class="list-group-item media border-bottom">
                        <div class="d-inline-block custom-control custom-switch ml-3">
                            <input type="checkbox" class="custom-control-input" id="customSwitch2" v-model="largerText">
                            <label class="custom-control-label" for="customSwitch2"></label>
                        </div>
                        <div class="d-inline-block ml-3 font-weight-bold">
                            Larger Text
                        </div>
                    </div>
                    <!-- <div class="list-group-item media border-bottom">
                    <div class="d-inline-block custom-control custom-switch ml-3">
                    <input type="checkbox" class="custom-control-input" id="customSwitch3" v-model="autoRefresh">
                    <label class="custom-control-label" for="customSwitch3"></label>
                    </div>
                    <div class="d-inline-block ml-3 font-weight-bold">
                    Auto Refresh
                    </div>
                    </div> -->
                    <div class="list-group-item media border-bottom d-flex align-items-center">
                        <div class="d-inline-block custom-control custom-switch ml-3">
                            <input type="checkbox" class="custom-control-input" id="customSwitch4" v-model="mutedNotifications">
                            <label class="custom-control-label" for="customSwitch4"></label>
                        </div>
                        <div class="d-inline-block ml-3 font-weight-bold">
                            Mute Notifications 
                            <p class="small mb-0">You will not receive any direct message notifications from <strong>{{thread.username}}</strong>.</p>
                        </div>
                    </div>
                </ul>
            </div>
        </div>
    </div>
    <b-modal ref="ctxModal"
    id="ctx-modal"
    hide-header
    hide-footer
    centered
    rounded
    size="sm"
    body-class="list-group-flush p-0 rounded">
    <div class="list-group text-center">
        <div v-if="ctxContext && ctxContext.type == 'photo'" class="list-group-item rounded cursor-pointer font-weight-bold text-dark" @click="viewOriginal()">View Original</div>
        <div v-if="ctxContext && ctxContext.type == 'video'" class="list-group-item rounded cursor-pointer font-weight-bold text-dark" @click="viewOriginal()">Play</div>
        <div v-if="ctxContext && ctxContext.type == 'link'" class="list-group-item rounded cursor-pointer" @click="clickLink()">
            <p class="mb-0" style="font-size:12px;">
                Navigate to 
            </p>
            <p class="mb-0 font-weight-bold text-dark">
                {{this.ctxContext.meta.domain}}
            </p>
        </div>
        <div v-if="ctxContext && (ctxContext.type == 'text' || ctxContext.type == 'emoji' || ctxContext.type == 'link')" class="list-group-item rounded cursor-pointer text-dark" @click="copyText()">Copy</div>
        <div v-if="ctxContext && !ctxContext.isAuthor" class="list-group-item rounded cursor-pointer text-muted" @click="reportMessage()">Report</div>
        <div v-if="ctxContext && ctxContext.isAuthor" class="list-group-item rounded cursor-pointer text-muted" @click="deleteMessage()">Delete</div>
        <div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
    </div>
    </b-modal>
</div>
</template>

<style type="text/css" scoped>
    .reply-btn {
        position: absolute;
        bottom: 54px;
        right: 20px;
        width: 90px;
        text-align: center;
        border-radius: 0 3px 3px 0;
    }
    .media-body .bg-primary {
        background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
    }
    .pill-to {
        background:#EDF2F7;
        font-weight: 500;
        border-radius: 20px !important;
        padding-left: 1rem;
        padding-right: 1rem;
        padding-top: 0.5rem;
        padding-bottom: 0.5rem;
        margin-right: 3rem;
        margin-bottom: 0.25rem;
    }
    .pill-from {
        color: white !important;
        text-align: right !important;
        /*background: #53d769;*/
        background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
        font-weight: 500;
        border-radius: 20px !important;
        padding-left: 1rem;
        padding-right: 1rem;
        padding-top: 0.5rem;
        padding-bottom: 0.5rem;
        margin-left: 3rem;
        margin-bottom: 0.25rem;
    }
    .chat-msg:hover {
        background: #f7fbfd;
    }
    .no-focus:focus {
        outline: none !important;
        outline-width: 0 !important;
        box-shadow: none;
        -moz-box-shadow: none;
        -webkit-box-shadow: none;
    }
    .emoji-msg {
        font-size: 4rem !important;
        line-height: 30px !important;
        margin-top: 10px !important;
    }
    .larger-text {
        font-size: 22px;
    }
</style>

<script type="text/javascript">
    export default {
        props: ['accountId'],
        data() {
            return {
                config: window.App.config,
                hideAvatars: true,
                hideTimestamps: false,
                largerText: false,
                autoRefresh: false,
                mutedNotifications: false,
                blocked: false,
                loaded: false,
                profile: {},
                page: 'read',
                pages: ['browse', 'add', 'read'],
                threads: [],
                thread: false,
                threadIndex: false,

                replyText: '',
                composeUsername: '',

                ctxContext: null,
                ctxIndex: null,

                uploading: false,
                uploadProgress: null,

                min_id: null,
                max_id: null,
                loadingMessages: false,
                showLoadMore: true,
            }
        },

        mounted() {
            this.fetchProfile();
            let self = this;
            axios.get('/api/direct/thread', {
                params: {
                    pid: self.accountId
                }
            })
            .then(res => {
                self.loaded = true;
                let d = res.data;
                d.messages.reverse();
                this.thread = d;
                this.threads = [d];
                this.threadIndex = 0;
                let mids = d.messages.map(m => m.id);
                this.max_id = Math.max(...mids);
                this.min_id = Math.min(...mids);
                this.mutedNotifications = d.muted;
                this.markAsRead();
                //this.messagePoll();
                setTimeout(function() {
                    let objDiv = document.querySelector('.dm-wrapper');
                    objDiv.scrollTop = objDiv.scrollHeight;
                }, 300);
            });

            let options = localStorage.getItem('px_dm_options');
            if(options) {
                options = JSON.parse(options);
                this.hideAvatars = options.hideAvatars;
                this.hideTimestamps = options.hideTimestamps;
                this.largerText = options.largerText;
            }
        },

        watch: {
            mutedNotifications: function(v) {
                if(v) {
                    axios.post('/api/direct/mute', {
                        id: this.accountId
                    }).then(res => {

                    });
                } else {
                    axios.post('/api/direct/unmute', {
                        id: this.accountId
                    }).then(res => {

                    });
                }
                this.mutedNotifications = v;
            },

            hideAvatars: function(v) {
                this.hideAvatars = v;
                this.updateOptions();
            },

            hideTimestamps: function(v) {
                this.hideTimestamps = v;
                this.updateOptions();
            },

            largerText: function(v) {
                this.largerText = v;
                this.updateOptions();
            },
        },

        updated() {
            $('[data-toggle="tooltip"]').tooltip();
        },

        methods: {
            fetchProfile() {
                axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
                    this.profile = res.data;
                    window._sharedData.curUser = res.data;
                    window.App.util.navatar();
                });
            },

            sendMessage() {
                let self = this;
                let rt = this.replyText;
                axios.post('/api/direct/create', {
                    'to_id': this.threads[this.threadIndex].id,
                    'message': rt,
                    'type': self.isEmoji(rt) && rt.length < 10 ? 'emoji' : 'text'
                }).then(res => {
                    let msg = res.data;
                    self.threads[self.threadIndex].messages.push(msg);
                    let mids = self.threads[self.threadIndex].messages.map(m => m.id);
                    this.max_id = Math.max(...mids)
                    this.min_id = Math.min(...mids)
                    setTimeout(function() {
                        var objDiv = document.querySelector('.dm-wrapper');
                        objDiv.scrollTop = objDiv.scrollHeight;
                    }, 300);
                }).catch(err => {
                    if(err.response.status == 403) {
                        self.blocked = true;
                        swal('Profile Unavailable', 'You cannot message this profile at this time.', 'error');
                    }
                })
                this.replyText = '';
            },

            openCtxMenu(r, i) {
                this.ctxIndex = i;
                this.ctxContext = r;
                this.$refs.ctxModal.show();
            },

            closeCtxMenu() {
                this.$refs.ctxModal.hide();
            },

            truncate(t) {
                return _.truncate(t);
            },

            deleteMessage() {
                let self = this;
                let c = window.confirm('Are you sure you want to delete this message?');
                if(c) {
                    axios.delete('/api/direct/message', {
                        params: {
                            id: self.ctxContext.reportId
                        }
                    }).then(res => {
                        self.threads[self.threadIndex].messages.splice(self.ctxIndex,1);
                        self.closeCtxMenu();
                    });
                } else {
                    self.closeCtxMenu();
                }
            },

            reportMessage() {
                this.closeCtxMenu();
                let url = '/i/report?type=post&id=' + this.ctxContext.reportId;
                window.location.href = url;
                return;
            },

            uploadMedia(event) {
                let self = this;
                $(document).on('change', '#uploadMedia', function(e) {
                    self.handleUpload();
                });
                let el = $(event.target);
                el.attr('disabled', '');
                $('#uploadMedia').click();
                el.blur();
                el.removeAttr('disabled');
            },

            handleUpload() {
                let self = this;
                self.uploading = true;
                let io = document.querySelector('#uploadMedia');
                if(!io.files.length) {
                    this.uploading = false;
                }
                Array.prototype.forEach.call(io.files, function(io, i) {
                    let type = io.type;
                    let acceptedMimes = self.config.uploader.media_types.split(',');
                    let validated = $.inArray(type, acceptedMimes);
                    if(validated == -1) {
                        swal('Invalid File Type', 'The file you are trying to add is not a valid mime type. Please upload a '+self.config.uploader.media_types+' only.', 'error');
                        self.uploading = false;
                        return;
                    }

                    let form = new FormData();
                    form.append('file', io);
                    form.append('to_id', self.threads[self.threadIndex].id);

                    let xhrConfig = {
                        onUploadProgress: function(e) {
                            let progress = Math.round( (e.loaded * 100) / e.total );
                            self.uploadProgress = progress;
                        }
                    };

                    axios.post('/api/direct/media', form, xhrConfig)
                    .then(function(e) {
                        self.uploadProgress = 100;
                        self.uploading = false;
                        let msg = {
                            id: e.data.id,
                            type: e.data.type,
                            reportId: e.data.reportId,
                            isAuthor: true,
                            text: null,
                            media: e.data.url,
                            timeAgo: '1s',
                            seen: null
                        };
                        self.threads[self.threadIndex].messages.push(msg);
                        setTimeout(function() {
                            var objDiv = document.querySelector('.dm-wrapper');
                            objDiv.scrollTop = objDiv.scrollHeight;
                        }, 300);

                    }).catch(function(e) {
                        switch(e.response.status) {
                            case 451:
                            self.uploading = false;
                            io.value = null;
                            swal('Banned Content', 'This content has been banned and cannot be uploaded.', 'error');
                            break;

                            default:
                            self.uploading = false;
                            io.value = null;
                            swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
                            break;
                        }
                    });
                    io.value = null;
                    self.uploadProgress = 0;
                });
            },

            viewOriginal() {
                let url = this.ctxContext.media;
                window.location.href = url;
                return;
            },

            isEmoji(text) {
                const onlyEmojis = text.replace(new RegExp('[\u0000-\u1eeff]', 'g'), '')
                const visibleChars = text.replace(new RegExp('[\n\r\s]+|( )+', 'g'), '')
                return onlyEmojis.length === visibleChars.length
            },

            copyText() {
                window.App.util.clipboard(this.ctxContext.text);
                this.closeCtxMenu();
                return;
            },

            clickLink() {
                let url = this.ctxContext.text;
                if(this.ctxContext.meta.local != true) {
                    url = '/i/redirect?url=' + encodeURI(this.ctxContext.text);
                }
                window.location.href = url;
            },

            markAsRead() {
                return;
                axios.post('/api/direct/read', {
                    pid: this.accountId,
                    sid: this.max_id
                }).then(res => {
                }).catch(err => {
                });
            },

            loadOlderMessages() {
                let self = this;
                this.loadingMessages = true;

                axios.get('/api/direct/thread', {
                    params: {
                        pid: this.accountId,
                        max_id: this.min_id,
                    }
                }).then(res => {
                    let d = res.data;
                    if(!d.messages.length) {
                        this.showLoadMore = false;
                        this.loadingMessages = false;
                        return;
                    }
                    let cids = this.thread.messages.map(m => m.id);
                    let m = d.messages.filter(m => {
                        return cids.indexOf(m.id) == -1;
                    }).reverse();
                    let mids = m.map(m => m.id);
                    let min_id = Math.min(...mids);
                    if(min_id == this.min_id) {
                        this.showLoadMore = false;
                        this.loadingMessages = false;
                        return;
                    }
                    this.min_id = min_id;
                    this.thread.messages.unshift(...m);
                    setTimeout(function() {
                        self.loadingMessages = false;
                    }, 500);
                }).catch(err => {
                    this.loadingMessages = false;
                })
            },

            messagePoll() {
                let self = this;
                setInterval(function() {
                    axios.get('/api/direct/thread', {
                        params: {
                            pid: self.accountId,
                            min_id: self.thread.messages[self.thread.messages.length - 1].id
                        }
                    }).then(res => {
                    });
                }, 5000);
            },

            showOptions() {
                this.page = 'options';
            },

            updateOptions() {
                let options = {
                    v: 1,
                    hideAvatars: this.hideAvatars,
                    hideTimestamps: this.hideTimestamps,
                    largerText: this.largerText
                }
                window.localStorage.setItem('px_dm_options', JSON.stringify(options));
            }
        }
    }
</script>