diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4c253aa7..e76df3e0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -85,6 +85,14 @@ module ApplicationHelper
tag(:meta, content: content, property: property)
end
+ def react_component(name, props = {}, &block)
+ if block.nil?
+ content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) })
+ else
+ content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block)
+ end
+ end
+
def body_classes
output = (@body_classes || '').split(' ')
output << "theme-#{current_theme.parameterize}"
diff --git a/app/javascript/gabsocial/containers/media_container.js b/app/javascript/gabsocial/containers/media_container.js
index 3fc9d2ca..b622f9b7 100644
--- a/app/javascript/gabsocial/containers/media_container.js
+++ b/app/javascript/gabsocial/containers/media_container.js
@@ -1,11 +1,80 @@
-import Poll from 'gabsocial/components/poll'
+import React, { PureComponent, Fragment } from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import MediaGallery from 'gabsocial/components/media_gallery';
+import Video from 'gabsocial/components/video';
+import Card from 'gabsocial/components/status_card';
+import Poll from 'gabsocial/components/poll';
+import { List as ImmutableList, fromJS } from 'immutable';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
export default class MediaContainer extends PureComponent {
+ static propTypes = {
+ locale: PropTypes.string.isRequired,
+ components: PropTypes.object.isRequired,
+ };
+
+ state = {
+ media: null,
+ index: null,
+ time: null,
+ };
+
+ handleOpenMedia = (media, index) => {
+ document.body.classList.add('with-modals--active');
+ this.setState({ media, index });
+ }
+
+ handleOpenVideo = (video, time) => {
+ const media = ImmutableList([video]);
+
+ document.body.classList.add('with-modals--active');
+ this.setState({ media, time });
+ }
+
+ handleCloseMedia = () => {
+ document.body.classList.remove('with-modals--active');
+ this.setState({ media: null, index: null, time: null });
+ }
+
render () {
+ const { locale, components } = this.props;
+
return (
-
- )
+
+
+ {[].map.call(components, (component, i) => {
+ const componentName = component.getAttribute('data-component');
+ const Component = MEDIA_COMPONENTS[componentName];
+ const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
+
+ Object.assign(props, {
+ ...(media ? { media: fromJS(media) } : {}),
+ ...(card ? { card: fromJS(card) } : {}),
+ ...(poll ? { poll: fromJS(poll) } : {}),
+
+ ...(componentName === 'Video' ? {
+ onOpenVideo: this.handleOpenVideo,
+ } : {
+ onOpenMedia: this.handleOpenMedia,
+ }),
+ });
+
+ return ReactDOM.createPortal(
+ ,
+ component,
+ );
+ })}
+
+
+ );
}
}
\ No newline at end of file
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index e2acb321..0754cb06 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -1,29 +1,239 @@
+'use strict';
+
+import escapeTextContentForBrowser from 'escape-html';
import loadPolyfills from '../gabsocial/load_polyfills';
import ready from '../gabsocial/ready';
import { start } from '../gabsocial/common';
start();
-/**
- * : todo : deleting this unused main() [the import of MediaContainer] or MediaContainer or the Poll import in MediaContainer causes this error:
- * commenting out or putting return; works fine. but cannot delete that import or reactComponents variable for some reason.
- * Webpacker::Manifest::MissingEntryError at /settings/preferences
- * Webpacker can't find common in gab-social/public/packs/manifest.json. Possible causes:
- * 1. You want to set webpacker.yml value of compile to true for your environment
- * unless you are using the `webpack -w` or the webpack-dev-server.
- * 2. webpack has not yet re-run to reflect updates.
- * 3. You have misconfigured Webpacker's config/webpacker.yml file.
- * 4. Your webpack configuration is not creating a manifest.
- */
+window.addEventListener('message', e => {
+ const data = e.data || {};
+
+ if (!window.parent || data.type !== 'setHeight') {
+ return;
+ }
+
+ ready(( ) => {
+ window.parent.postMessage({
+ type: 'setHeight',
+ id: data.id,
+ height: document.getElementsByTagName('html')[0].scrollHeight,
+ }, '*');
+ });
+});
+
function main ( ) {
- ready(() => {
- const reactComponents = document.querySelectorAll('[data-component]');
- if (reactComponents.length > 0) {
- import(/* webpackChunkName: "defunct/media_container" */ '../gabsocial/containers/media_container').then(() => {})
+ const IntlMessageFormat = require('intl-messageformat').default;
+ const { timeAgoString } = require('../gabsocial/components/relative_timestamp');
+ const { delegate } = require('rails-ujs');
+ // const emojify = require('../gabsocial/components/emoji/emoji').default;
+ const { getLocale } = require('../gabsocial/locales');
+ const { messages } = getLocale();
+ //(Rjc) 2019-05-24 defined but never used
+ // const React = require('react');
+ const ReactDOM = require('react-dom');
+ const createHistory = require('history').createBrowserHistory;
+
+ const scrollToDetailedStatus = () => {
+ const history = createHistory();
+ const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
+ const location = history.location;
+
+ if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
+ detailedStatuses[0].scrollIntoView();
+ history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
}
+ };
+
+ ready(() => {
+ const locale = document.documentElement.lang;
+
+ const dateTimeFormat = new Intl.DateTimeFormat(locale, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ });
+
+ // [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
+ // content.innerHTML = emojify(content.innerHTML);
+ // });
+
+ [].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
+ const datetime = new Date(content.getAttribute('datetime'));
+ const formattedDate = dateTimeFormat.format(datetime);
+
+ content.title = formattedDate;
+ content.textContent = formattedDate;
+ });
+
+ [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
+ const datetime = new Date(content.getAttribute('datetime'));
+ const now = new Date();
+
+ content.title = dateTimeFormat.format(datetime);
+ content.textContent = timeAgoString({
+ formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values),
+ formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
+ }, datetime, now, now.getFullYear());
+ });
+
+ const reactComponents = document.querySelectorAll('[data-component]');
+
+ if (reactComponents.length > 0) {
+ import(/* webpackChunkName: "containers/media_container" */ '../gabsocial/containers/media_container')
+ .then(({ default: MediaContainer }) => {
+ [].forEach.call(reactComponents, (component) => {
+ [].forEach.call(component.children, (child) => {
+ component.removeChild(child);
+ });
+ });
+ const content = document.createElement('div');
+
+ ReactDOM.render(, content);
+ document.body.appendChild(content);
+ scrollToDetailedStatus();
+ })
+ .catch(error => {
+ console.error(error);
+ scrollToDetailedStatus();
+ });
+ } else {
+ scrollToDetailedStatus();
+ }
+
+ if (document.body.classList.contains('with-modals')) {
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+ const scrollbarWidthStyle = document.createElement('style');
+ scrollbarWidthStyle.id = 'scrollbar-width';
+ document.head.appendChild(scrollbarWidthStyle);
+ scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0);
+ }
+ });
+
+ delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
+ if (button !== 0) {
+ return true;
+ }
+ window.location.href = target.href;
+ return false;
+ });
+
+ delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
+ const contentEl = target.parentNode.parentNode.querySelector('.e-content');
+
+ if (contentEl.style.display === 'block') {
+ contentEl.style.display = 'none';
+ target.parentNode.style.marginBottom = 0;
+ } else {
+ contentEl.style.display = 'block';
+ target.parentNode.style.marginBottom = null;
+ }
+
+ return false;
+ });
+
+ delegate(document, '.modal-button', 'click', e => {
+ e.preventDefault();
+
+ let href;
+
+ if (e.target.nodeName !== 'A') {
+ href = e.target.parentNode.href;
+ } else {
+ href = e.target.href;
+ }
+
+ window.open(href, 'gabsocial-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
+ });
+
+ delegate(document, '#account_display_name', 'input', ({ target }) => {
+ const name = document.querySelector('.card .display-name strong');
+ if (name) {
+ if (target.value) {
+ // name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
+ } else {
+ name.textContent = document.querySelector('#default_account_display_name').textContent;
+ }
+ }
+ });
+
+ delegate(document, '#account_avatar', 'change', ({ target }) => {
+ const avatar = document.querySelector('.card .avatar img');
+ const [file] = target.files || [];
+ const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
+
+ avatar.src = url;
+ });
+
+ const getProfileAvatarAnimationHandler = (swapTo) => {
+ //animate avatar gifs on the profile page when moused over
+ return ({ target }) => {
+ const swapSrc = target.getAttribute(swapTo);
+ //only change the img source if autoplay is off and the image src is actually different
+ if(target.getAttribute('data-autoplay') === 'false' && target.src !== swapSrc) {
+ target.src = swapSrc;
+ }
+ };
+ };
+
+ delegate(document, 'img#profile_page_avatar', 'mouseover', getProfileAvatarAnimationHandler('data-original'));
+
+ delegate(document, 'img#profile_page_avatar', 'mouseout', getProfileAvatarAnimationHandler('data-static'));
+
+ delegate(document, '#account_header', 'change', ({ target }) => {
+ const header = document.querySelector('.card .card__img img');
+ const [file] = target.files || [];
+ const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
+
+ header.src = url;
+ });
+
+ delegate(document, '#account_locked', 'change', ({ target }) => {
+ const lock = document.querySelector('.card .display-name i');
+
+ if (target.checked) {
+ lock.style.display = 'inline';
+ } else {
+ lock.style.display = 'none';
+ }
+ });
+
+ delegate(document, '.input-copy input', 'click', ({ target }) => {
+ target.focus();
+ target.select();
+ target.setSelectionRange(0, target.value.length);
+ });
+
+ delegate(document, '.input-copy button', 'click', ({ target }) => {
+ const input = target.parentNode.querySelector('.input-copy__wrapper input');
+
+ const oldReadOnly = input.readonly;
+
+ input.readonly = false;
+ input.focus();
+ input.select();
+ input.setSelectionRange(0, input.value.length);
+
+ try {
+ if (document.execCommand('copy')) {
+ input.blur();
+ target.parentNode.classList.add('copied');
+
+ setTimeout(() => {
+ target.parentNode.classList.remove('copied');
+ }, 700);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ input.readonly = oldReadOnly;
});
}
-loadPolyfills().catch((error) => {
+loadPolyfills().then(main).catch(error => {
console.error(error);
});
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index 77408bcd..b3c14512 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -11,6 +11,13 @@
%strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)}
= Formatter.instance.format(status.proper, custom_emojify: true)
+ - unless status.proper.media_attachments.empty?
+ - if status.proper.media_attachments.first.video?
+ - video = status.proper.media_attachments.first
+ = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description
+ - else
+ = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
+
.detailed-status__meta
= link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 39f28682..226ed5f8 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -22,6 +22,20 @@
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
+ - if status.preloadable_poll
+ = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
+ = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
+ - elsif !status.media_attachments.empty?
+ - if status.media_attachments.first.video?
+ - video = status.media_attachments.first
+ = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
+ = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
+ - else
+ = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
+ = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
+ - elsif status.preview_card
+ = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+
.detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 }
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index 61d28c06..0df7497e 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -26,6 +26,20 @@
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
+ - if status.preloadable_poll
+ = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
+ = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
+ - elsif !status.media_attachments.empty?
+ - if status.media_attachments.first.video?
+ - video = status.media_attachments.first
+ = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
+ = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
+ - else
+ = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
+ = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
+ - elsif status.preview_card
+ = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+
.status__action-bar
.status__action-bar__counter
= link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do