<article class="status-article {{originalStatus.visibility === 'direct' ? 'status-direct' : ''}} {{isStatusInContext ? 'status-in-context' : ''}}" tabindex="0" aria-posinset="{{index}}" aria-setsize="{{length}}" on:recalculateHeight> {{#if status.reblog}} <div class="status-boosted"> <svg> <use xlink:href="#fa-retweet" /> </svg> <span> <a href="/accounts/{{status.account.id}}">{{status.account.username}}</a> boosted </span> </div> {{/if}} <div class="status-author"> <a class="status-author-name" href="/accounts/{{originalAccount.id}}"> {{originalAccount.display_name || originalAccount.username}} </a> <span class="status-author-handle"> {{'@' + originalAccount.acct}} </span> {{#if isStatusInContext}} <ExternalLink class="status-author-date" href="{{originalStatus.url}}" showIcon="true"> <time datetime={{createdAtDate}} title="{{relativeDate}}">{{relativeDate}}</time> </ExternalLink> {{else}} <a class="status-author-date" href="/statuses/{{originalStatus.id}}"> <time datetime={{createdAtDate}} title="{{relativeDate}}">{{relativeDate}}</time> </a> {{/if}} </div> <Avatar account={{originalAccount}} className="status-sidebar"/> {{#if originalStatus.spoiler_text}} <div class="status-spoiler">{{originalStatus.spoiler_text}}</div> {{/if}} {{#if originalStatus.spoiler_text}} <div class="status-spoiler-button"> <button type="button" on:click="onClickSpoilerButton()">{{spoilerShown ? 'Show less' : 'Show more'}}</button> </div> {{/if}} {{#if !originalStatus.spoiler_text || spoilerShown}} <div class="status-content" ref:contentNode> {{{emojifiedContent}}} </div> {{/if}} {{#if originalMediaAttachments && originalMediaAttachments.length}} {{#if originalStatus.sensitive}} <div class="status-sensitive-media-container {{sensitiveShown ? 'status-sensitive-media-shown' : 'status-sensitive-media-hidden'}}" > <button type="button" class="status-sensitive-media-button" aria-label="{{sensitiveShown ? 'Hide sensitive media: ' : 'Show sensitive media'}}" on:click="onClickSensitiveMediaButton()" > {{#if !sensitiveShown}} <div class="status-sensitive-media-warning"> <span> Sensitive content. Click to show. </span> </div> {{/if}} <div class="svg-wrapper"> <svg> <use xlink:href="{{sensitiveShown ? '#fa-eye-slash' : '#fa-eye'}}" /> </svg> </div> </button> {{#if sensitiveShown}} <Media mediaAttachments="{{originalMediaAttachments}}" sensitive="{{originalStatus.sensitive}}"/> {{/if}} </div> {{else}} <Media mediaAttachments="{{originalMediaAttachments}}" sensitive="{{originalStatus.sensitive}}"/> {{/if}} {{/if}} <Toolbar :status /> </article> <style> .status-article { width: 560px; max-width: calc(100vw - 40px); padding: 10px 20px; display: grid; grid-template-areas: ".............. status-boosted" "status-sidebar status-author" "status-sidebar status-spoiler" "status-sidebar status-spoiler-button" "status-sidebar status-content" "status-media status-media" ".............. status-toolbar"; grid-template-columns: 58px 1fr; border-bottom: 1px solid var(--main-border); } .status-article.status-direct { background-color: var(--status-direct-background); } :global(.status-sidebar) { grid-area: status-sidebar; margin: 0 10px 0 0; } .status-spoiler { grid-area: status-spoiler; word-wrap: break-word; overflow: hidden; white-space: pre-wrap; font-size: 1.1em; margin: 5px; } .status-spoiler-button { grid-area: status-spoiler-button; margin: 5px; } .status-spoiler-button button { padding: 5px 10px; font-size: 1.1em; } .status-author { grid-area: status-author; display: flex; align-items: center; margin: 0 10px 0 5px; font-size: 1.1em; min-width: 0; } :global(.status-author a, .status-author a:visited, .status-author a:hover, .status-author .status-author-handle) { color: var(--deemphasized-text-color); } :global(.status-author .status-author-name) { min-width: 0; flex-shrink: 1; color: var(--body-text-color); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } :global(.status-author .status-author-name:hover) { color: var(--body-text-color); } .status-author-handle { min-width: 0; flex: 1; margin-left: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } :global(.status-author-date) { color: var(--deemphasized-text-color); flex-shrink: 1; text-align: right; margin-left: 5px; white-space: nowrap; } :global(.status-author-date:hover) { color: var(--deemphasized-text-color); } .status-content { margin: 10px 10px 10px 5px; grid-area: status-content; word-wrap: break-word; overflow: hidden; white-space: pre-wrap; font-size: 0.9em; } .status-in-context .status-content { font-size: 1.3em; margin: 20px 10px 20px 5px; } :global(.status-content .status-emoji) { width: 20px; height: 20px; margin: -3px 0; } :global(.status-content p) { margin: 0 0 20px; } :global(.status-content p:first-child) { margin: 0 0 20px; } :global(.status-content p:last-child) { margin: 0; } .status-boosted span { margin-left: 5px; } :global(.status-boosted span, .status-boosted a, .status-boosted a:visited, .status-boosted a:hover) { color: var(--deemphasized-text-color); } .status-boosted { grid-area: status-boosted; margin: 5px 10px 5px 5px; display: flex; align-items: center; } .status-boosted svg { width: 18px; height: 18px; fill: var(--deemphasized-text-color); } .status-sensitive-media-container { grid-area: status-media; margin: 10px 0; position: relative; border-radius: 0; border: none; background: none; } .status-sensitive-media-button { margin: 0; padding: 0; border: none; background: none; width: 100%; height: 100%; } .status-sensitive-media-button:hover { background: none; } .status-sensitive-media-button:active { background: none; } .status-sensitive-media-shown .status-sensitive-media-button { position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 90; } .status-sensitive-media-container.status-sensitive-media-hidden { width: 100%; margin: 10px auto; height: 200px; } .status-sensitive-media-container .status-sensitive-media-warning { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; color: var(--deemphasized-text-color); z-index: 60; } .status-sensitive-media-container .svg-wrapper { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: flex-start; justify-content: flex-start; z-index: 40; pointer-events: none; background: var(--mask-bg); } .status-sensitive-media-container.status-sensitive-media-shown .svg-wrapper { background: none; } .status-sensitive-media-container svg { width: 24px; height: 24px; fill: var(--mask-svg-fill); border-radius: 2px; background: var(--mask-opaque-bg); margin: 1px; padding: 6px 10px; } .status-sensitive-media-container.status-sensitive-media-hidden svg { fill: var(--deemphasized-text-color); background: var(--mask-opaque-bg); } </style> <script> import Avatar from './Avatar.html' import Media from './Media.html' import Toolbar from './Toolbar.html' import { mark, stop } from '../../_utils/marks' import IntlRelativeFormat from 'intl-relativeformat' import { replaceAll } from '../../_utils/strings' import { store } from '../../_store/store' import ExternalLink from '../ExternalLink.html' const relativeFormat = new IntlRelativeFormat('en-US'); export default { oncreate() { this.hydrateContent() }, components: { Avatar, Media, Toolbar, ExternalLink }, store: () => store, computed: { isStatusInContext: (timelineType, timelineValue, statusId) => { return timelineType === 'status' && timelineValue === statusId }, createdAtDate: (status) => status.created_at, relativeDate: (createdAtDate) => { mark('compute relativeDate') let res = relativeFormat.format(new Date(createdAtDate)) stop('compute relativeDate') return res }, originalStatus: (status) => status.reblog ? status.reblog : status, originalAccount: (originalStatus) => originalStatus.account, originalMediaAttachments: (originalStatus) => originalStatus.media_attachments, statusId: (originalStatus) => originalStatus.id, emojifiedContent: (originalStatus) => { let status = originalStatus let content = status.content if (status.emojis && status.emojis.length) { for (let emoji of status.emojis) { let { shortcode, url } = emoji let shortcodeWithColons = `:${shortcode}:` content = replaceAll( content, shortcodeWithColons, `<img class="status-emoji" draggable="false" src="${url}" alt="${shortcodeWithColons}" title="${shortcodeWithColons}" />` ) } } return content }, spoilerShown: ($spoilersShown, $currentInstance, statusId) => $spoilersShown && $spoilersShown[$currentInstance] && $spoilersShown[$currentInstance][statusId], sensitiveShown: ($sensitivesShown, $currentInstance, statusId) => $sensitivesShown && $sensitivesShown[$currentInstance] && $sensitivesShown[$currentInstance][statusId], }, methods: { onClickSpoilerButton() { let statusId = this.get('statusId') let instanceName = this.store.get('currentInstance') let $spoilersShown = this.store.get('spoilersShown') || {} if (!$spoilersShown[instanceName]) { $spoilersShown[instanceName] = {} } $spoilersShown[instanceName][statusId] = !$spoilersShown[instanceName][statusId] this.store.set({'spoilersShown': $spoilersShown}) this.hydrateContent() this.fire('recalculateHeight') }, onClickSensitiveMediaButton() { let statusId = this.get('statusId') let instanceName = this.store.get('currentInstance') let $sensitivesShown = this.store.get('sensitivesShown') || {} if (!$sensitivesShown[instanceName]) { $sensitivesShown[instanceName] = {} } $sensitivesShown[instanceName][statusId] = !$sensitivesShown[instanceName][statusId] this.store.set({'sensitivesShown': $sensitivesShown}) this.fire('recalculateHeight') }, hydrateContent() { if (!this.refs.contentNode) { return } let status = this.get('originalStatus') mark('hydrateContent') if (status.tags && status.tags.length) { let anchorTags = Array.from(this.refs.contentNode.querySelectorAll( 'a[class~=hashtag][href^=http]')) for (let tag of status.tags) { for (let anchorTag of anchorTags) { if (anchorTag.getAttribute('href').endsWith(`/tags/${tag.name}`)) { anchorTag.setAttribute('href', `/tags/${tag.name}`) anchorTag.removeAttribute('target') anchorTag.removeAttribute('rel') } } } } if (status.mentions && status.mentions.length) { let anchorTags = Array.from(this.refs.contentNode.querySelectorAll( 'a[class~=mention][href^=http]')) for (let mention of status.mentions) { for (let anchorTag of anchorTags) { if (anchorTag.getAttribute('href') === mention.url) { anchorTag.setAttribute('href', `/accounts/${mention.id}`) anchorTag.removeAttribute('target') anchorTag.removeAttribute('rel') } } } } stop('hydrateContent') } } } </script>