DirectMessage.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. <template>
  2. <div>
  3. <div v-if="loaded && page == 'read'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
  4. <div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
  5. <div class="card shadow-none border mt-4">
  6. <div class="card-header bg-white d-flex justify-content-between align-items-center">
  7. <span>
  8. <a href="/account/direct" class="text-muted">
  9. <i class="fas fa-chevron-left fa-lg"></i>
  10. </a>
  11. </span>
  12. <span>
  13. <div class="media">
  14. <img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="40" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
  15. <div class="media-body">
  16. <p class="mb-0">
  17. <span class="font-weight-bold">{{thread.name}}</span>
  18. </p>
  19. <p class="mb-0">
  20. <a v-if="!thread.isLocal" :href="'/'+thread.username" class="text-decoration-none text-muted">{{thread.username}}</a>
  21. <a v-else :href="'/'+thread.username" class="text-decoration-none text-muted">&commat;{{thread.username}}</a>
  22. </p>
  23. </div>
  24. </div>
  25. </span>
  26. <span><a href="#" class="text-muted" @click.prevent="showOptions()"><i class="fas fa-cog fa-lg"></i></a></span>
  27. </div>
  28. <ul class="list-group list-group-flush dm-wrapper" style="height:60vh;overflow-y: scroll;">
  29. <li class="list-group-item border-0">
  30. <p class="text-center small text-muted">
  31. Conversation with <span class="font-weight-bold">{{thread.username}}</span>
  32. </p>
  33. <hr>
  34. </li>
  35. <li v-if="showLoadMore && thread.messages && thread.messages.length > 5" class="list-group-item border-0 mt-n4">
  36. <p class="text-center small text-muted">
  37. <button v-if="!loadingMessages" class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" @click="loadOlderMessages()">Load Older Messages</button>
  38. <button v-else class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" disabled>Loading...</button>
  39. </p>
  40. </li>
  41. <li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)">
  42. <div v-if="!convo.isAuthor" class="media d-inline-flex mb-0">
  43. <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';">
  44. <div class="media-body">
  45. <p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
  46. <img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
  47. </p>
  48. <div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
  49. <div class="media-body">
  50. <div class="card mb-2 rounded border shadow" style="width:240px;" :title="convo.text">
  51. <div class="card-body p-0">
  52. <div class="media d-flex align-items-center">
  53. <div v-if="convo.meta.local" class="bg-primary mr-3 border-right p-3">
  54. <i class="fas fa-link text-white fa-2x"></i>
  55. </div>
  56. <div v-else class="bg-light mr-3 border-right p-3">
  57. <i class="fas fa-link text-lighter fa-2x"></i>
  58. </div>
  59. <div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
  60. {{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. <p v-else-if="convo.type == 'video'" class="pill-to p-0 shadow">
  68. <!-- <video :src="convo.media" width="140px" style="border-radius:20px;"></video> -->
  69. <span class="d-block bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px;border-radius: 20px;">
  70. <div class="text-center">
  71. <p class="mb-1">
  72. <i class="fas fa-play fa-2x text-white"></i>
  73. </p>
  74. <p class="mb-0 small font-weight-bold text-white">
  75. Play
  76. </p>
  77. </div>
  78. </span>
  79. </p>
  80. <p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
  81. {{convo.text}}
  82. </p>
  83. <p v-else-if="convo.type == 'story:react'" class="pill-to p-0 shadow" style="width: 140px;margin-bottom: 10px;position:relative;">
  84. <img :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
  85. <span class="badge badge-light rounded-pill border" style="font-size: 20px;position: absolute;bottom:-10px;left:-10px;">
  86. {{convo.meta.reaction}}
  87. </span>
  88. </p>
  89. <span v-else-if="convo.type == 'story:comment'" class="p-0" style="display: flex;justify-content: flex-start;margin-bottom: 10px;position:relative;">
  90. <span class="">
  91. <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';">
  92. <span class="pill-to shadow text-break" style="width:fit-content;">{{convo.meta.caption}}</span>
  93. </span>
  94. </span>
  95. <p v-else :class="[largerText ? 'pill-to shadow larger-text text-break':'pill-to shadow text-break']">
  96. {{convo.text}}
  97. </p>
  98. <p v-if="convo.type == 'story:react'" class="small text-muted mb-0 ml-0">
  99. <span class="font-weight-bold">{{ convo.meta.story_actor_username }}</span> reacted your story
  100. </p>
  101. <p v-if="convo.type == 'story:comment'" class="small text-muted mb-0 ml-0">
  102. <span class="font-weight-bold">{{ convo.meta.story_actor_username }}</span> replied to your story
  103. </p>
  104. <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>
  105. <p v-else>&nbsp;</p>
  106. </div>
  107. </div>
  108. <div v-else class="media d-inline-flex float-right mb-0 mr-2">
  109. <div class="media-body">
  110. <p v-if="convo.type == 'photo'" class="pill-from p-0 shadow">
  111. <img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
  112. </p>
  113. <div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
  114. <div class="media-body">
  115. <div class="card mb-2 rounded border shadow" style="width:240px;" :title="convo.text">
  116. <div class="card-body p-0">
  117. <div class="media d-flex align-items-center">
  118. <div v-if="convo.meta.local" class="bg-primary mr-3 border-right p-3">
  119. <i class="fas fa-link text-white fa-2x"></i>
  120. </div>
  121. <div v-else class="bg-light mr-3 border-right p-3">
  122. <i class="fas fa-link text-lighter fa-2x"></i>
  123. </div>
  124. <div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
  125. {{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. <p v-else-if="convo.type == 'video'" class="pill-from p-0 shadow">
  133. <!-- <video :src="convo.media" width="140px" style="border-radius:20px;"></video> -->
  134. <span class="rounded-pill bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px">
  135. <div class="text-center">
  136. <p class="mb-1">
  137. <i class="fas fa-play fa-2x text-white"></i>
  138. </p>
  139. <p class="mb-0 small font-weight-bold">
  140. Play
  141. </p>
  142. </div>
  143. </span>
  144. </p>
  145. <p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
  146. {{convo.text}}
  147. </p>
  148. <p v-else-if="convo.type == 'story:react'" class="pill-from p-0 shadow" style="margin-bottom: 10px;position:relative;width:fit-content;">
  149. <img :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
  150. <span class="badge badge-light rounded-pill border" style="font-size: 20px;position: absolute;bottom:-10px;right:-10px;">
  151. {{convo.meta.reaction}}
  152. </span>
  153. </p>
  154. <span v-else-if="convo.type == 'story:comment'" class="p-0" style="display: flex;justify-content: flex-end;margin-bottom: 10px;position:relative;">
  155. <span class="d-flex align-items-end flex-column">
  156. <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';">
  157. <span class="pill-from shadow text-break" style="width:fit-content;">{{convo.meta.caption}}</span>
  158. </span>
  159. </span>
  160. <p v-else :class="[largerText ? 'pill-from shadow larger-text text-break':'pill-from shadow text-break']">
  161. {{convo.text}}
  162. </p>
  163. <p v-if="convo.type == 'story:react'" class="small text-muted text-right mb-0 mr-0">
  164. You reacted to <span class="font-weight-bold">{{ convo.meta.story_username }}</span>'s story
  165. </p>
  166. <p v-if="convo.type == 'story:comment'" class="small text-muted text-right mb-0 mr-0">
  167. You replied to <span class="font-weight-bold">{{ convo.meta.story_username }}</span>'s story
  168. </p>
  169. <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}}
  170. </p>
  171. <p v-else>&nbsp;</p>
  172. </div>
  173. <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';">
  174. </div>
  175. </li>
  176. </ul>
  177. <div class="card-footer bg-white p-0">
  178. <form class="border-0 rounded-0 align-middle" method="post" action="#">
  179. <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>
  180. <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"/>
  181. </form>
  182. </div>
  183. <div class="card-footer p-0">
  184. <p class="d-flex justify-content-between align-items-center mb-0 px-3 py-1 small">
  185. <!-- <span class="font-weight-bold" style="color: #D69E2E">
  186. <i class="fas fa-circle mr-1"></i>
  187. Typing ...
  188. </span> -->
  189. <span>
  190. <!-- <span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
  191. <i class="fas fa-share mr-1"></i>
  192. Share
  193. </span> -->
  194. <span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
  195. <i class="fas fa-upload mr-1"></i>
  196. Add Photo/Video
  197. </span>
  198. </span>
  199. <input type="file" id="uploadMedia" class="d-none" name="uploadMedia" accept="image/jpeg,image/png,image/gif,video/mp4" >
  200. <span class="text-muted font-weight-bold">{{replyText.length}}/600</span>
  201. </p>
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. <div v-if="loaded && page == 'options'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
  207. <div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
  208. <div class="card shadow-none border mt-4">
  209. <div class="card-header bg-white d-flex justify-content-between align-items-center">
  210. <span>
  211. <a href="#" class="text-muted" @click.prevent="page='read'">
  212. <i class="fas fa-chevron-left fa-lg"></i>
  213. </a>
  214. </span>
  215. <span>
  216. <p class="mb-0 lead font-weight-bold py-2">Message Settings</p>
  217. </span>
  218. <span class="text-lighter" data-toggle="tooltip" data-placement="bottom" title="Have a nice day!"><i class="far fa-smile fa-lg"></i></span>
  219. </div>
  220. <ul class="list-group list-group-flush dm-wrapper" style="height: 698px;">
  221. <div class="list-group-item media border-bottom">
  222. <div class="d-inline-block custom-control custom-switch ml-3">
  223. <input type="checkbox" class="custom-control-input" id="customSwitch0" v-model="hideAvatars">
  224. <label class="custom-control-label" for="customSwitch0"></label>
  225. </div>
  226. <div class="d-inline-block ml-3 font-weight-bold">
  227. Hide Avatars
  228. </div>
  229. </div>
  230. <div class="list-group-item media border-bottom">
  231. <div class="d-inline-block custom-control custom-switch ml-3">
  232. <input type="checkbox" class="custom-control-input" id="customSwitch1" v-model="hideTimestamps">
  233. <label class="custom-control-label" for="customSwitch1"></label>
  234. </div>
  235. <div class="d-inline-block ml-3 font-weight-bold">
  236. Hide Timestamps
  237. </div>
  238. </div>
  239. <div class="list-group-item media border-bottom">
  240. <div class="d-inline-block custom-control custom-switch ml-3">
  241. <input type="checkbox" class="custom-control-input" id="customSwitch2" v-model="largerText">
  242. <label class="custom-control-label" for="customSwitch2"></label>
  243. </div>
  244. <div class="d-inline-block ml-3 font-weight-bold">
  245. Larger Text
  246. </div>
  247. </div>
  248. <!-- <div class="list-group-item media border-bottom">
  249. <div class="d-inline-block custom-control custom-switch ml-3">
  250. <input type="checkbox" class="custom-control-input" id="customSwitch3" v-model="autoRefresh">
  251. <label class="custom-control-label" for="customSwitch3"></label>
  252. </div>
  253. <div class="d-inline-block ml-3 font-weight-bold">
  254. Auto Refresh
  255. </div>
  256. </div> -->
  257. <div class="list-group-item media border-bottom d-flex align-items-center">
  258. <div class="d-inline-block custom-control custom-switch ml-3">
  259. <input type="checkbox" class="custom-control-input" id="customSwitch4" v-model="mutedNotifications">
  260. <label class="custom-control-label" for="customSwitch4"></label>
  261. </div>
  262. <div class="d-inline-block ml-3 font-weight-bold">
  263. Mute Notifications
  264. <p class="small mb-0">You will not receive any direct message notifications from <strong>{{thread.username}}</strong>.</p>
  265. </div>
  266. </div>
  267. </ul>
  268. </div>
  269. </div>
  270. </div>
  271. <b-modal ref="ctxModal"
  272. id="ctx-modal"
  273. hide-header
  274. hide-footer
  275. centered
  276. rounded
  277. size="sm"
  278. body-class="list-group-flush p-0 rounded">
  279. <div class="list-group text-center">
  280. <div v-if="ctxContext && ctxContext.type == 'photo'" class="list-group-item rounded cursor-pointer font-weight-bold text-dark" @click="viewOriginal()">View Original</div>
  281. <div v-if="ctxContext && ctxContext.type == 'video'" class="list-group-item rounded cursor-pointer font-weight-bold text-dark" @click="viewOriginal()">Play</div>
  282. <div v-if="ctxContext && ctxContext.type == 'link'" class="list-group-item rounded cursor-pointer" @click="clickLink()">
  283. <p class="mb-0" style="font-size:12px;">
  284. Navigate to
  285. </p>
  286. <p class="mb-0 font-weight-bold text-dark">
  287. {{this.ctxContext.meta.domain}}
  288. </p>
  289. </div>
  290. <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>
  291. <div v-if="ctxContext && !ctxContext.isAuthor" class="list-group-item rounded cursor-pointer text-muted" @click="reportMessage()">Report</div>
  292. <div v-if="ctxContext && ctxContext.isAuthor" class="list-group-item rounded cursor-pointer text-muted" @click="deleteMessage()">Delete</div>
  293. <div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
  294. </div>
  295. </b-modal>
  296. </div>
  297. </template>
  298. <style type="text/css" scoped>
  299. .reply-btn {
  300. position: absolute;
  301. bottom: 54px;
  302. right: 20px;
  303. width: 90px;
  304. text-align: center;
  305. border-radius: 0 3px 3px 0;
  306. }
  307. .media-body .bg-primary {
  308. background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
  309. }
  310. .pill-to {
  311. background:#EDF2F7;
  312. font-weight: 500;
  313. border-radius: 20px !important;
  314. padding-left: 1rem;
  315. padding-right: 1rem;
  316. padding-top: 0.5rem;
  317. padding-bottom: 0.5rem;
  318. margin-right: 3rem;
  319. margin-bottom: 0.25rem;
  320. }
  321. .pill-from {
  322. color: white !important;
  323. text-align: right !important;
  324. /*background: #53d769;*/
  325. background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
  326. font-weight: 500;
  327. border-radius: 20px !important;
  328. padding-left: 1rem;
  329. padding-right: 1rem;
  330. padding-top: 0.5rem;
  331. padding-bottom: 0.5rem;
  332. margin-left: 3rem;
  333. margin-bottom: 0.25rem;
  334. }
  335. .chat-msg:hover {
  336. background: #f7fbfd;
  337. }
  338. .no-focus:focus {
  339. outline: none !important;
  340. outline-width: 0 !important;
  341. box-shadow: none;
  342. -moz-box-shadow: none;
  343. -webkit-box-shadow: none;
  344. }
  345. .emoji-msg {
  346. font-size: 4rem !important;
  347. line-height: 30px !important;
  348. margin-top: 10px !important;
  349. }
  350. .larger-text {
  351. font-size: 22px;
  352. }
  353. </style>
  354. <script type="text/javascript">
  355. export default {
  356. props: ['accountId'],
  357. data() {
  358. return {
  359. config: window.App.config,
  360. hideAvatars: true,
  361. hideTimestamps: false,
  362. largerText: false,
  363. autoRefresh: false,
  364. mutedNotifications: false,
  365. blocked: false,
  366. loaded: false,
  367. profile: {},
  368. page: 'read',
  369. pages: ['browse', 'add', 'read'],
  370. threads: [],
  371. thread: false,
  372. threadIndex: false,
  373. replyText: '',
  374. composeUsername: '',
  375. ctxContext: null,
  376. ctxIndex: null,
  377. uploading: false,
  378. uploadProgress: null,
  379. min_id: null,
  380. max_id: null,
  381. loadingMessages: false,
  382. showLoadMore: true,
  383. }
  384. },
  385. mounted() {
  386. this.fetchProfile();
  387. let self = this;
  388. axios.get('/api/direct/thread', {
  389. params: {
  390. pid: self.accountId
  391. }
  392. })
  393. .then(res => {
  394. self.loaded = true;
  395. let d = res.data;
  396. d.messages.reverse();
  397. this.thread = d;
  398. this.threads = [d];
  399. this.threadIndex = 0;
  400. let mids = d.messages.map(m => m.id);
  401. this.max_id = Math.max(...mids);
  402. this.min_id = Math.min(...mids);
  403. this.mutedNotifications = d.muted;
  404. this.markAsRead();
  405. //this.messagePoll();
  406. setTimeout(function() {
  407. let objDiv = document.querySelector('.dm-wrapper');
  408. objDiv.scrollTop = objDiv.scrollHeight;
  409. }, 300);
  410. });
  411. let options = localStorage.getItem('px_dm_options');
  412. if(options) {
  413. options = JSON.parse(options);
  414. this.hideAvatars = options.hideAvatars;
  415. this.hideTimestamps = options.hideTimestamps;
  416. this.largerText = options.largerText;
  417. }
  418. },
  419. watch: {
  420. mutedNotifications: function(v) {
  421. if(v) {
  422. axios.post('/api/direct/mute', {
  423. id: this.accountId
  424. }).then(res => {
  425. });
  426. } else {
  427. axios.post('/api/direct/unmute', {
  428. id: this.accountId
  429. }).then(res => {
  430. });
  431. }
  432. this.mutedNotifications = v;
  433. },
  434. hideAvatars: function(v) {
  435. this.hideAvatars = v;
  436. this.updateOptions();
  437. },
  438. hideTimestamps: function(v) {
  439. this.hideTimestamps = v;
  440. this.updateOptions();
  441. },
  442. largerText: function(v) {
  443. this.largerText = v;
  444. this.updateOptions();
  445. },
  446. },
  447. updated() {
  448. $('[data-toggle="tooltip"]').tooltip();
  449. },
  450. methods: {
  451. fetchProfile() {
  452. axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
  453. this.profile = res.data;
  454. window._sharedData.curUser = res.data;
  455. window.App.util.navatar();
  456. });
  457. },
  458. sendMessage() {
  459. let self = this;
  460. let rt = this.replyText;
  461. axios.post('/api/direct/create', {
  462. 'to_id': this.threads[this.threadIndex].id,
  463. 'message': rt,
  464. 'type': self.isEmoji(rt) && rt.length < 10 ? 'emoji' : 'text'
  465. }).then(res => {
  466. let msg = res.data;
  467. self.threads[self.threadIndex].messages.push(msg);
  468. let mids = self.threads[self.threadIndex].messages.map(m => m.id);
  469. this.max_id = Math.max(...mids)
  470. this.min_id = Math.min(...mids)
  471. setTimeout(function() {
  472. var objDiv = document.querySelector('.dm-wrapper');
  473. objDiv.scrollTop = objDiv.scrollHeight;
  474. }, 300);
  475. }).catch(err => {
  476. if(err.response.status == 403) {
  477. self.blocked = true;
  478. swal('Profile Unavailable', 'You cannot message this profile at this time.', 'error');
  479. }
  480. })
  481. this.replyText = '';
  482. },
  483. openCtxMenu(r, i) {
  484. this.ctxIndex = i;
  485. this.ctxContext = r;
  486. this.$refs.ctxModal.show();
  487. },
  488. closeCtxMenu() {
  489. this.$refs.ctxModal.hide();
  490. },
  491. truncate(t) {
  492. return _.truncate(t);
  493. },
  494. deleteMessage() {
  495. let self = this;
  496. let c = window.confirm('Are you sure you want to delete this message?');
  497. if(c) {
  498. axios.delete('/api/direct/message', {
  499. params: {
  500. id: self.ctxContext.reportId
  501. }
  502. }).then(res => {
  503. self.threads[self.threadIndex].messages.splice(self.ctxIndex,1);
  504. self.closeCtxMenu();
  505. });
  506. } else {
  507. self.closeCtxMenu();
  508. }
  509. },
  510. reportMessage() {
  511. this.closeCtxMenu();
  512. let url = '/i/report?type=post&id=' + this.ctxContext.reportId;
  513. window.location.href = url;
  514. return;
  515. },
  516. uploadMedia(event) {
  517. let self = this;
  518. $(document).on('change', '#uploadMedia', function(e) {
  519. self.handleUpload();
  520. });
  521. let el = $(event.target);
  522. el.attr('disabled', '');
  523. $('#uploadMedia').click();
  524. el.blur();
  525. el.removeAttr('disabled');
  526. },
  527. handleUpload() {
  528. let self = this;
  529. self.uploading = true;
  530. let io = document.querySelector('#uploadMedia');
  531. if(!io.files.length) {
  532. this.uploading = false;
  533. }
  534. Array.prototype.forEach.call(io.files, function(io, i) {
  535. let type = io.type;
  536. let acceptedMimes = self.config.uploader.media_types.split(',');
  537. let validated = $.inArray(type, acceptedMimes);
  538. if(validated == -1) {
  539. 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');
  540. self.uploading = false;
  541. return;
  542. }
  543. let form = new FormData();
  544. form.append('file', io);
  545. form.append('to_id', self.threads[self.threadIndex].id);
  546. let xhrConfig = {
  547. onUploadProgress: function(e) {
  548. let progress = Math.round( (e.loaded * 100) / e.total );
  549. self.uploadProgress = progress;
  550. }
  551. };
  552. axios.post('/api/direct/media', form, xhrConfig)
  553. .then(function(e) {
  554. self.uploadProgress = 100;
  555. self.uploading = false;
  556. let msg = {
  557. id: e.data.id,
  558. type: e.data.type,
  559. reportId: e.data.reportId,
  560. isAuthor: true,
  561. text: null,
  562. media: e.data.url,
  563. timeAgo: '1s',
  564. seen: null
  565. };
  566. self.threads[self.threadIndex].messages.push(msg);
  567. setTimeout(function() {
  568. var objDiv = document.querySelector('.dm-wrapper');
  569. objDiv.scrollTop = objDiv.scrollHeight;
  570. }, 300);
  571. }).catch(function(e) {
  572. switch(e.response.status) {
  573. case 451:
  574. self.uploading = false;
  575. io.value = null;
  576. swal('Banned Content', 'This content has been banned and cannot be uploaded.', 'error');
  577. break;
  578. default:
  579. self.uploading = false;
  580. io.value = null;
  581. swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
  582. break;
  583. }
  584. });
  585. io.value = null;
  586. self.uploadProgress = 0;
  587. });
  588. },
  589. viewOriginal() {
  590. let url = this.ctxContext.media;
  591. window.location.href = url;
  592. return;
  593. },
  594. isEmoji(text) {
  595. const onlyEmojis = text.replace(new RegExp('[\u0000-\u1eeff]', 'g'), '')
  596. const visibleChars = text.replace(new RegExp('[\n\r\s]+|( )+', 'g'), '')
  597. return onlyEmojis.length === visibleChars.length
  598. },
  599. copyText() {
  600. window.App.util.clipboard(this.ctxContext.text);
  601. this.closeCtxMenu();
  602. return;
  603. },
  604. clickLink() {
  605. let url = this.ctxContext.text;
  606. if(this.ctxContext.meta.local != true) {
  607. url = '/i/redirect?url=' + encodeURI(this.ctxContext.text);
  608. }
  609. window.location.href = url;
  610. },
  611. markAsRead() {
  612. return;
  613. axios.post('/api/direct/read', {
  614. pid: this.accountId,
  615. sid: this.max_id
  616. }).then(res => {
  617. }).catch(err => {
  618. });
  619. },
  620. loadOlderMessages() {
  621. let self = this;
  622. this.loadingMessages = true;
  623. axios.get('/api/direct/thread', {
  624. params: {
  625. pid: this.accountId,
  626. max_id: this.min_id,
  627. }
  628. }).then(res => {
  629. let d = res.data;
  630. if(!d.messages.length) {
  631. this.showLoadMore = false;
  632. this.loadingMessages = false;
  633. return;
  634. }
  635. let cids = this.thread.messages.map(m => m.id);
  636. let m = d.messages.filter(m => {
  637. return cids.indexOf(m.id) == -1;
  638. }).reverse();
  639. let mids = m.map(m => m.id);
  640. let min_id = Math.min(...mids);
  641. if(min_id == this.min_id) {
  642. this.showLoadMore = false;
  643. this.loadingMessages = false;
  644. return;
  645. }
  646. this.min_id = min_id;
  647. this.thread.messages.unshift(...m);
  648. setTimeout(function() {
  649. self.loadingMessages = false;
  650. }, 500);
  651. }).catch(err => {
  652. this.loadingMessages = false;
  653. })
  654. },
  655. messagePoll() {
  656. let self = this;
  657. setInterval(function() {
  658. axios.get('/api/direct/thread', {
  659. params: {
  660. pid: self.accountId,
  661. min_id: self.thread.messages[self.thread.messages.length - 1].id
  662. }
  663. }).then(res => {
  664. });
  665. }, 5000);
  666. },
  667. showOptions() {
  668. this.page = 'options';
  669. },
  670. updateOptions() {
  671. let options = {
  672. v: 1,
  673. hideAvatars: this.hideAvatars,
  674. hideTimestamps: this.hideTimestamps,
  675. largerText: this.largerText
  676. }
  677. window.localStorage.setItem('px_dm_options', JSON.stringify(options));
  678. }
  679. }
  680. }
  681. </script>