about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/features/ui/components/zoomable_image.js165
-rw-r--r--app/javascript/styles/mastodon/about.scss48
-rw-r--r--app/javascript/styles/mastodon/components.scss3
-rw-r--r--app/views/about/show.html.haml22
4 files changed, 145 insertions, 93 deletions
diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.js b/app/javascript/mastodon/features/ui/components/zoomable_image.js
index 0cae0862d..0a0a4d41a 100644
--- a/app/javascript/mastodon/features/ui/components/zoomable_image.js
+++ b/app/javascript/mastodon/features/ui/components/zoomable_image.js
@@ -1,10 +1,16 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Hammer from 'hammerjs';
 
 const MIN_SCALE = 1;
 const MAX_SCALE = 4;
-const DOUBLE_TAP_SCALE = 2;
+
+const getMidpoint = (p1, p2) => ({
+  x: (p1.clientX + p2.clientX) / 2,
+  y: (p1.clientY + p2.clientY) / 2,
+});
+
+const getDistance = (p1, p2) =>
+  Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
 
 const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
 
@@ -31,95 +37,81 @@ export default class ZoomableImage extends React.PureComponent {
   removers = [];
   container = null;
   image = null;
-  lastScale = null;
-  zoomCenter = null;
+  lastTouchEndTime = 0;
+  lastDistance = 0;
 
   componentDidMount () {
-    // register pinch event handlers to the container
-    let hammer = new Hammer.Manager(this.container, {
-      // required to make container scrollable by touch
-      touchAction: 'pan-x pan-y',
-    });
-    hammer.add(new Hammer.Pinch());
-    hammer.on('pinchstart', this.handlePinchStart);
-    hammer.on('pinchmove', this.handlePinchMove);
-    this.removers.push(() => hammer.off('pinchstart pinchmove'));
-
-    // register tap event handlers
-    hammer = new Hammer.Manager(this.image);
-    // NOTE the order of adding is also the order of gesture recognition
-    hammer.add(new Hammer.Tap({ event: 'doubletap', taps: 2 }));
-    hammer.add(new Hammer.Tap());
-    // prevent the 'tap' event handler be fired on double tap
-    hammer.get('tap').requireFailure('doubletap');
-    // NOTE 'tap' and 'doubletap' events are fired by touch and *mouse*
-    hammer.on('tap', this.handleTap);
-    hammer.on('doubletap', this.handleDoubleTap);
-    this.removers.push(() => hammer.off('tap doubletap'));
+    let handler = this.handleTouchStart;
+    this.container.addEventListener('touchstart', handler);
+    this.removers.push(() => this.container.removeEventListener('touchstart', handler));
+    handler = this.handleTouchMove;
+    // on Chrome 56+, touch event listeners will default to passive
+    // https://www.chromestatus.com/features/5093566007214080
+    this.container.addEventListener('touchmove', handler, { passive: false });
+    this.removers.push(() => this.container.removeEventListener('touchend', handler));
   }
 
   componentWillUnmount () {
     this.removeEventListeners();
   }
 
-  componentDidUpdate (prevProps, prevState) {
-    if (!this.zoomCenter) return;
-
-    const { x: cx, y: cy } = this.zoomCenter;
-    const { scale: prevScale } = prevState;
-    const { scale: nextScale } = this.state;
-    const { scrollLeft, scrollTop } = this.container;
-
-    // math memo:
-    // x = (scrollLeft + cx) / scrollWidth
-    // x' = (nextScrollLeft + cx) / nextScrollWidth
-    // scrollWidth = clientWidth * prevScale
-    // scrollWidth' = clientWidth * nextScale
-    // Solve x = x' for nextScrollLeft
-    const nextScrollLeft = (scrollLeft + cx) * nextScale / prevScale - cx;
-    const nextScrollTop = (scrollTop + cy) * nextScale / prevScale - cy;
-
-    this.container.scrollLeft = nextScrollLeft;
-    this.container.scrollTop = nextScrollTop;
-  }
-
   removeEventListeners () {
     this.removers.forEach(listeners => listeners());
     this.removers = [];
   }
 
-  handleClick = e => {
-    // prevent the click event propagated to parent
-    e.stopPropagation();
+  handleTouchStart = e => {
+    if (e.touches.length !== 2) return;
 
-    // the tap event handler is executed at the same time by touch and mouse,
-    // so we don't need to execute the onClick handler here
+    this.lastDistance = getDistance(...e.touches);
   }
 
-  handlePinchStart = () => {
-    this.lastScale = this.state.scale;
-  }
+  handleTouchMove = e => {
+    const { scrollTop, scrollHeight, clientHeight } = this.container;
+    if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
+      // prevent propagating event to MediaModal
+      e.stopPropagation();
+      return;
+    }
+    if (e.touches.length !== 2) return;
 
-  handlePinchMove = e => {
-    const scale = clamp(MIN_SCALE, MAX_SCALE, this.lastScale * e.scale);
-    this.zoom(scale, e.center);
-  }
+    e.preventDefault();
+    e.stopPropagation();
 
-  handleTap = () => {
-    const handler = this.props.onClick;
-    if (handler) handler();
+    const distance = getDistance(...e.touches);
+    const midpoint = getMidpoint(...e.touches);
+    const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
+
+    this.zoom(scale, midpoint);
+
+    this.lastMidpoint = midpoint;
+    this.lastDistance = distance;
   }
 
-  handleDoubleTap = e => {
-    if (this.state.scale === MIN_SCALE)
-      this.zoom(DOUBLE_TAP_SCALE, e.center);
-    else
-      this.zoom(MIN_SCALE, e.center);
+  zoom(nextScale, midpoint) {
+    const { scale } = this.state;
+    const { scrollLeft, scrollTop } = this.container;
+
+    // math memo:
+    // x = (scrollLeft + midpoint.x) / scrollWidth
+    // x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
+    // scrollWidth = clientWidth * scale
+    // scrollWidth' = clientWidth * nextScale
+    // Solve x = x' for nextScrollLeft
+    const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
+    const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
+
+    this.setState({ scale: nextScale }, () => {
+      this.container.scrollLeft = nextScrollLeft;
+      this.container.scrollTop = nextScrollTop;
+    });
   }
 
-  zoom (scale, center) {
-    this.zoomCenter = center;
-    this.setState({ scale });
+  handleClick = e => {
+    // don't propagate event to MediaModal
+    e.stopPropagation();
+    const handler = this.props.onClick;
+    if (handler) handler();
   }
 
   setContainerRef = c => {
@@ -134,18 +126,6 @@ export default class ZoomableImage extends React.PureComponent {
     const { alt, src } = this.props;
     const { scale } = this.state;
     const overflow = scale === 1 ? 'hidden' : 'scroll';
-    const marginStyle = {
-      position: 'absolute',
-      top: 0,
-      bottom: 0,
-      left: 0,
-      right: 0,
-      display: 'flex',
-      alignItems: 'center',
-      justifyContent: 'center',
-      transform: `scale(${scale})`,
-      transformOrigin: '0 0',
-    };
 
     return (
       <div
@@ -153,18 +133,17 @@ export default class ZoomableImage extends React.PureComponent {
         ref={this.setContainerRef}
         style={{ overflow }}
       >
-        <div
-          className='zoomable-image__margin'
-          style={marginStyle}
-        >
-          <img
-            ref={this.setImageRef}
-            role='presentation'
-            alt={alt}
-            src={src}
-            onClick={this.handleClick}
-          />
-        </div>
+        <img
+          role='presentation'
+          ref={this.setImageRef}
+          alt={alt}
+          src={src}
+          style={{
+            transform: `scale(${scale})`,
+            transformOrigin: '0 0',
+          }}
+          onClick={this.handleClick}
+        />
       </div>
     );
   }
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index c484f074b..03211036c 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -681,6 +681,54 @@ $small-breakpoint: 960px;
       margin-bottom: 0;
     }
 
+    .account {
+      border-bottom: 0;
+      padding: 0;
+
+      &__display-name {
+        align-items: center;
+        display: flex;
+        margin-right: 5px;
+      }
+
+      div.account__display-name {
+        &:hover {
+          .display-name strong {
+            text-decoration: none;
+          }
+        }
+
+        .account__avatar {
+          cursor: default;
+        }
+      }
+
+      &__avatar-wrapper {
+        margin-left: 0;
+        flex: 0 0 auto;
+      }
+
+      &__avatar {
+        width: 44px;
+        height: 44px;
+        background-size: 44px 44px;
+      }
+
+      .display-name {
+        font-size: 15px;
+
+        &__account {
+          font-size: 14px;
+        }
+      }
+    }
+
+    @media screen and (max-width: $small-breakpoint) {
+      .contact {
+        margin-top: 30px;
+      }
+    }
+
     @media screen and (max-width: $column-breakpoint) {
       padding: 25px 20px;
     }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 31089301c..c82a760c4 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1483,6 +1483,9 @@
   position: relative;
   width: 100%;
   height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 
   img {
     max-width: $media-modal-media-max-width;
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 5594abecb..674771cbc 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -107,6 +107,28 @@
               %div
                 %h3= t 'about.what_is_mastodon'
                 %p= t 'about.about_mastodon_html'
+              %div.contact
+                %h3= t 'about.administered_by'
+
+                .account
+                  .account__wrapper
+                    - if @instance_presenter.contact_account
+                      = link_to TagManager.instance.url_for(@instance_presenter.contact_account), class: 'account__display-name' do
+                        .account__avatar-wrapper
+                          .account__avatar{ style: "background-image: url(#{@instance_presenter.contact_account.avatar.url})" }
+                        %span.display-name
+                          %bdi
+                            %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account)
+                          %span.display-name__account @#{@instance_presenter.contact_account.acct}
+                    - else
+                      .account__display-name
+                        .account__avatar-wrapper
+                          .account__avatar{ style: "background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})" }
+                        %span.display-name
+                          %strong= t 'about.contact_missing'
+                          %span.display-name__account= t 'about.contact_unavailable'
+
+                    = link_to t('about.learn_more'), about_more_path, class: 'button button-alternative'
 
             = render 'features'