about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2023-04-22 19:13:02 +0200
committerGitHub <noreply@github.com>2023-04-22 19:13:02 +0200
commit2ebbfebfe9c7c967e3bcc9da0eec4628f9188233 (patch)
treeed3fa3f23d520e99fdd8b19597992b59a47aaa7d /app/javascript/flavours
parentf30c5e7f15f967019245d2c78f3c2e89800eb838 (diff)
parent9ef32ea570fd0db63bd75714cd847abad6833345 (diff)
Merge pull request #2192 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/components/animated_number.jsx76
-rw-r--r--app/javascript/flavours/glitch/components/animated_number.tsx58
-rw-r--r--app/javascript/flavours/glitch/components/gifv.jsx76
-rw-r--r--app/javascript/flavours/glitch/components/gifv.tsx68
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx2
-rw-r--r--app/javascript/flavours/glitch/features/status/index.jsx10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.jsx2
-rw-r--r--app/javascript/flavours/glitch/packs/public.jsx50
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss86
11 files changed, 255 insertions, 177 deletions
diff --git a/app/javascript/flavours/glitch/components/animated_number.jsx b/app/javascript/flavours/glitch/components/animated_number.jsx
deleted file mode 100644
index dd21d97f0..000000000
--- a/app/javascript/flavours/glitch/components/animated_number.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ShortNumber from 'mastodon/components/short_number';
-import TransitionMotion from 'react-motion/lib/TransitionMotion';
-import spring from 'react-motion/lib/spring';
-import { reduceMotion } from 'flavours/glitch/initial_state';
-
-const obfuscatedCount = count => {
-  if (count < 0) {
-    return 0;
-  } else if (count <= 1) {
-    return count;
-  } else {
-    return '1+';
-  }
-};
-
-export default class AnimatedNumber extends React.PureComponent {
-
-  static propTypes = {
-    value: PropTypes.number.isRequired,
-    obfuscate: PropTypes.bool,
-  };
-
-  state = {
-    direction: 1,
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.value > this.props.value) {
-      this.setState({ direction: 1 });
-    } else if (nextProps.value < this.props.value) {
-      this.setState({ direction: -1 });
-    }
-  }
-
-  willEnter = () => {
-    const { direction } = this.state;
-
-    return { y: -1 * direction };
-  };
-
-  willLeave = () => {
-    const { direction } = this.state;
-
-    return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) };
-  };
-
-  render () {
-    const { value, obfuscate } = this.props;
-    const { direction } = this.state;
-
-    if (reduceMotion) {
-      return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />;
-    }
-
-    const styles = [{
-      key: `${value}`,
-      data: value,
-      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
-    }];
-
-    return (
-      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
-        {items => (
-          <span className='animated-number'>
-            {items.map(({ key, data, style }) => (
-              <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
-            ))}
-          </span>
-        )}
-      </TransitionMotion>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx
new file mode 100644
index 000000000..1673ff41b
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/animated_number.tsx
@@ -0,0 +1,58 @@
+import React, { useCallback, useState } from 'react';
+import ShortNumber from './short_number';
+import { TransitionMotion, spring } from 'react-motion';
+import { reduceMotion } from '../initial_state';
+
+const obfuscatedCount = (count: number) => {
+  if (count < 0) {
+    return 0;
+  } else if (count <= 1) {
+    return count;
+  } else {
+    return '1+';
+  }
+};
+
+type Props = {
+  value: number;
+  obfuscate?: boolean;
+}
+export const AnimatedNumber: React.FC<Props> = ({
+  value,
+  obfuscate,
+})=> {
+  const [previousValue, setPreviousValue] = useState(value);
+  const [direction, setDirection] = useState<1|-1>(1);
+
+  if (previousValue !== value) {
+    setPreviousValue(value);
+    setDirection(value > previousValue ? 1 : -1);
+  }
+
+  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
+  const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]);
+
+  if (reduceMotion) {
+    return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
+  }
+
+  const styles = [{
+    key: `${value}`,
+    data: value,
+    style: { y: spring(0, { damping: 35, stiffness: 400 }) },
+  }];
+
+  return (
+    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
+      {items => (
+        <span className='animated-number'>
+          {items.map(({ key, data, style }) => (
+            <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
+          ))}
+        </span>
+      )}
+    </TransitionMotion>
+  );
+};
+
+export default AnimatedNumber;
diff --git a/app/javascript/flavours/glitch/components/gifv.jsx b/app/javascript/flavours/glitch/components/gifv.jsx
deleted file mode 100644
index 1ce7e7c29..000000000
--- a/app/javascript/flavours/glitch/components/gifv.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export default class GIFV extends React.PureComponent {
-
-  static propTypes = {
-    src: PropTypes.string.isRequired,
-    alt: PropTypes.string,
-    lang: PropTypes.string,
-    width: PropTypes.number,
-    height: PropTypes.number,
-    onClick: PropTypes.func,
-  };
-
-  state = {
-    loading: true,
-  };
-
-  handleLoadedData = () => {
-    this.setState({ loading: false });
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.src !== this.props.src) {
-      this.setState({ loading: true });
-    }
-  }
-
-  handleClick = e => {
-    const { onClick } = this.props;
-
-    if (onClick) {
-      e.stopPropagation();
-      onClick();
-    }
-  };
-
-  render () {
-    const { src, width, height, alt, lang } = this.props;
-    const { loading } = this.state;
-
-    return (
-      <div className='gifv' style={{ position: 'relative' }}>
-        {loading && (
-          <canvas
-            width={width}
-            height={height}
-            role='button'
-            tabIndex={0}
-            aria-label={alt}
-            title={alt}
-            lang={lang}
-            onClick={this.handleClick}
-          />
-        )}
-
-        <video
-          src={src}
-          role='button'
-          tabIndex={0}
-          aria-label={alt}
-          title={alt}
-          lang={lang}
-          muted
-          loop
-          autoPlay
-          playsInline
-          onClick={this.handleClick}
-          onLoadedData={this.handleLoadedData}
-          style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
-        />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx
new file mode 100644
index 000000000..8968170c5
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/gifv.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback, useState } from 'react';
+
+type Props = {
+  src: string;
+  key: string;
+  alt?: string;
+  lang?: string;
+  width: number;
+  height: number;
+  onClick?: () => void;
+}
+
+export const GIFV: React.FC<Props> = ({
+  src,
+  alt,
+  lang,
+  width,
+  height,
+  onClick,
+})=> {
+  const [loading, setLoading] = useState(true);
+
+  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
+    setLoading(false);
+  }, [setLoading]);
+
+  const handleClick: React.MouseEventHandler = useCallback((e) => {
+    if (onClick) {
+      e.stopPropagation();
+      onClick();
+    }
+  }, [onClick]);
+
+  return (
+    <div className='gifv' style={{ position: 'relative' }}>
+      {loading && (
+        <canvas
+          width={width}
+          height={height}
+          role='button'
+          tabIndex={0}
+          aria-label={alt}
+          title={alt}
+          lang={lang}
+          onClick={handleClick}
+        />
+      )}
+
+      <video
+        src={src}
+        role='button'
+        tabIndex={0}
+        aria-label={alt}
+        title={alt}
+        lang={lang}
+        muted
+        loop
+        autoPlay
+        playsInline
+        onClick={handleClick}
+        onLoadedData={handleLoadedData}
+        style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
+      />
+    </div>
+  );
+};
+
+export default GIFV;
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
index 63a331086..06984f3ad 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
@@ -203,7 +203,7 @@ class Conversation extends ImmutablePureComponent {
               parseClick={this.parseClick}
               expanded={isExpanded}
               onExpandedToggle={this.handleShowMore}
-              collapsable
+              collapsible
               media={media}
             />
 
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx
index c220d761f..5d1160039 100644
--- a/app/javascript/flavours/glitch/features/status/index.jsx
+++ b/app/javascript/flavours/glitch/features/status/index.jsx
@@ -63,6 +63,7 @@ const messages = defineMessages({
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
+  statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}' },
   detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
@@ -161,13 +162,14 @@ const truncate = (str, num) => {
   }
 };
 
-const titleFromStatus = status => {
+const titleFromStatus = (intl, status) => {
   const displayName = status.getIn(['account', 'display_name']);
   const username = status.getIn(['account', 'username']);
-  const prefix = displayName.trim().length === 0 ? username : displayName;
+  const user = displayName.trim().length === 0 ? username : displayName;
   const text = status.get('search_index');
+  const attachmentCount = status.get('media_attachments').size;
 
-  return `${prefix}: "${truncate(text, 30)}"`;
+  return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
 };
 
 class Status extends ImmutablePureComponent {
@@ -710,7 +712,7 @@ class Status extends ImmutablePureComponent {
         </ScrollContainer>
 
         <Helmet>
-          <title>{titleFromStatus(status)}</title>
+          <title>{titleFromStatus(intl, status)}</title>
           <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
         </Helmet>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
index 2d49312e5..440a6ac4b 100644
--- a/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
@@ -131,4 +131,4 @@ class FilterModal extends ImmutablePureComponent {
 
 }
 
-export default connect(injectIntl(FilterModal));
+export default connect()(injectIntl(FilterModal));
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
index a5637d31c..78aee8dfe 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
@@ -371,7 +371,7 @@ class FocalPointModal extends ImmutablePureComponent {
             {focals && (
               <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
                 {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
-                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />}
+                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
 
                 <div className='focal-point__preview'>
                   <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
index fd2bd43cf..6ca96b743 100644
--- a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
@@ -188,7 +188,7 @@ class MediaModal extends ImmutablePureComponent {
             src={image.get('url')}
             width={width}
             height={height}
-            key={image.get('preview_url')}
+            key={image.get('url')}
             alt={image.get('description')}
             lang={language}
             onClick={this.toggleNavigation}
diff --git a/app/javascript/flavours/glitch/packs/public.jsx b/app/javascript/flavours/glitch/packs/public.jsx
index 335a0710d..93b249bb4 100644
--- a/app/javascript/flavours/glitch/packs/public.jsx
+++ b/app/javascript/flavours/glitch/packs/public.jsx
@@ -2,6 +2,15 @@ import 'packs/public-path';
 import loadPolyfills from 'flavours/glitch/load_polyfills';
 import ready from 'flavours/glitch/ready';
 import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
+import axios from 'axios';
+import { throttle } from 'lodash';
+import { defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
+  passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' },
+  passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
+});
 
 function main() {
   const IntlMessageFormat = require('intl-messageformat').default;
@@ -9,7 +18,7 @@ function main() {
   const { delegate } = require('@rails/ujs');
   const emojify = require('flavours/glitch/features/emoji/emoji').default;
   const { getLocale } = require('locales');
-  const { messages } = getLocale();
+  const { localeData } = getLocale();
   const React = require('react');
   const ReactDOM = require('react-dom');
   const { createBrowserHistory } = require('history');
@@ -54,6 +63,11 @@ function main() {
       hour12: false,
     });
 
+    const formatMessage = ({ id, defaultMessage }, values) => {
+      const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
+      return messageFormat.format(values);
+    };
+
     [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
       content.innerHTML = emojify(content.innerHTML);
     });
@@ -73,7 +87,7 @@ function main() {
         date.getMonth() === today.getMonth() &&
         date.getFullYear() === today.getFullYear();
     };
-    const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale);
+    const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
 
     [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
       const datetime = new Date(content.getAttribute('datetime'));
@@ -99,7 +113,7 @@ function main() {
       const timeGiven = content.getAttribute('datetime').includes('T');
       content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
       content.textContent = timeAgoString({
-        formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values),
+        formatMessage,
         formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
       }, datetime, now, now.getFullYear(), timeGiven);
     });
@@ -128,17 +142,19 @@ function main() {
       scrollToDetailedStatus();
     }
 
-    delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
-      const password = document.getElementById('registration_user_password');
-      const confirmation = document.getElementById('registration_user_password_confirmation');
-      if (confirmation.value && confirmation.value.length > password.maxLength) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
-      } else if (password.value && password.value !== confirmation.value) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
+    delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
+      const username = document.getElementById('user_account_attributes_username');
+
+      if (username.value && username.value.length > 0) {
+        axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
+          username.setCustomValidity(formatMessage(messages.usernameTaken));
+        }).catch(() => {
+          username.setCustomValidity('');
+        });
       } else {
-        confirmation.setCustomValidity('');
+        username.setCustomValidity('');
       }
-    });
+    }, 500, { leading: false, trailing: true }));
 
     delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
       const password = document.getElementById('user_password');
@@ -146,9 +162,9 @@ function main() {
       if (!confirmation) return;
 
       if (confirmation.value && confirmation.value.length > password.maxLength) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
+        confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
       } else if (password.value && password.value !== confirmation.value) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
+        confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
       } else {
         confirmation.setCustomValidity('');
       }
@@ -162,10 +178,10 @@ function main() {
 
       if (statusEl.dataset.spoiler === 'expanded') {
         statusEl.dataset.spoiler = 'folded';
-        this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
+        this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
       } else {
         statusEl.dataset.spoiler = 'expanded';
-        this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
+        this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
       }
 
       return false;
@@ -173,7 +189,7 @@ function main() {
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
       const statusEl = spoilerLink.parentNode.parentNode;
-      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index bb44d1bac..f69e8f276 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -1118,3 +1118,89 @@ code {
     white-space: nowrap;
   }
 }
+
+.progress-tracker {
+  display: flex;
+  align-items: center;
+  padding-bottom: 30px;
+  margin-bottom: 30px;
+
+  li {
+    flex: 0 0 auto;
+    position: relative;
+  }
+
+  .separator {
+    height: 2px;
+    background: $ui-base-lighter-color;
+    flex: 1 1 auto;
+
+    &.completed {
+      background: $highlight-text-color;
+    }
+  }
+
+  .circle {
+    box-sizing: border-box;
+    position: relative;
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px solid $ui-base-lighter-color;
+    flex: 0 0 auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 16px;
+    }
+  }
+
+  .label {
+    position: absolute;
+    font-size: 14px;
+    font-weight: 500;
+    color: $secondary-text-color;
+    padding-top: 10px;
+    text-align: center;
+    width: 100px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+
+  li:first-child .label {
+    left: auto;
+    inset-inline-start: 0;
+    text-align: start;
+    transform: none;
+  }
+
+  li:last-child .label {
+    left: auto;
+    inset-inline-end: 0;
+    text-align: end;
+    transform: none;
+  }
+
+  .active .circle {
+    border-color: $highlight-text-color;
+
+    &::before {
+      content: '';
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background: $highlight-text-color;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+
+  .completed .circle {
+    border-color: $highlight-text-color;
+    background: $highlight-text-color;
+  }
+}