diff --git a/app/javascript/flavours/glitch/components/media_gallery.jsx b/app/javascript/flavours/glitch/components/media_gallery.jsx
index 5be5fb4c58..1e40a26777 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.jsx
+++ b/app/javascript/flavours/glitch/components/media_gallery.jsx
@@ -11,6 +11,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
+import { formatTime } from 'flavours/glitch/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
@@ -58,7 +59,7 @@ class Item extends PureComponent {
hoverToPlay () {
const { attachment } = this.props;
- return !this.getAutoPlay() && attachment.get('type') === 'gifv';
+ return !this.getAutoPlay() && ['gifv', 'video'].includes(attachment.get('type'));
}
handleClick = (e) => {
@@ -152,10 +153,15 @@ class Item extends PureComponent {
/>
);
- } else if (attachment.get('type') === 'gifv') {
+ } else if (['gifv', 'video'].includes(attachment.get('type'))) {
const autoPlay = this.getAutoPlay();
+ const duration = attachment.getIn(['meta', 'original', 'duration']);
- badges.push(GIF);
+ if (attachment.get('type') === 'gifv') {
+ badges.push(GIF);
+ } else {
+ badges.push({formatTime(Math.floor(duration))});
+ }
thumbnail = (
@@ -169,6 +175,7 @@ class Item extends PureComponent {
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
+ onLoadedData={this.handleImageLoad}
autoPlay={autoPlay}
playsInline
loop
diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx
index a037895b4e..493ef4f68a 100644
--- a/app/javascript/flavours/glitch/components/status.jsx
+++ b/app/javascript/flavours/glitch/components/status.jsx
@@ -648,6 +648,27 @@ class Status extends ImmutablePureComponent {
media={status.get('media_attachments')}
/>,
);
+ } else if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
+ media.push(
+
+ {Component => (
+
+ )}
+ ,
+ );
+ mediaIcons.push('picture-o');
} else if (attachments.getIn([0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@@ -703,27 +724,6 @@ class Status extends ImmutablePureComponent {
,
);
mediaIcons.push('video-camera');
- } else { // Media type is 'image' or 'gifv'
- media.push(
-
- {Component => (
-
- )}
- ,
- );
- mediaIcons.push('picture-o');
}
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
deleted file mode 100644
index c709e58db1..0000000000
--- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import PropTypes from 'prop-types';
-
-import classNames from 'classnames';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import AudiotrackIcon from '@/material-icons/400-24px/music_note.svg?react';
-import PlayArrowIcon from '@/material-icons/400-24px/play_arrow.svg?react';
-import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
-import { Blurhash } from 'flavours/glitch/components/blurhash';
-import { Icon } from 'flavours/glitch/components/icon';
-import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
-
-export default class MediaItem extends ImmutablePureComponent {
-
- static propTypes = {
- attachment: ImmutablePropTypes.map.isRequired,
- displayWidth: PropTypes.number.isRequired,
- onOpenMedia: PropTypes.func.isRequired,
- };
-
- state = {
- visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
- loaded: false,
- };
-
- handleImageLoad = () => {
- this.setState({ loaded: true });
- };
-
- handleMouseEnter = e => {
- if (this.hoverToPlay()) {
- e.target.play();
- }
- };
-
- handleMouseLeave = e => {
- if (this.hoverToPlay()) {
- e.target.pause();
- e.target.currentTime = 0;
- }
- };
-
- hoverToPlay () {
- return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
- }
-
- handleClick = e => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
-
- if (this.state.visible) {
- this.props.onOpenMedia(this.props.attachment);
- } else {
- this.setState({ visible: true });
- }
- }
- };
-
- render () {
- const { attachment, displayWidth } = this.props;
- const { visible, loaded } = this.state;
-
- const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
- const height = width;
- const status = attachment.get('status');
- const title = status.get('spoiler_text') || attachment.get('description');
-
- let thumbnail, label, icon, content;
-
- if (!visible) {
- icon = (
-
-
-
- );
- } else {
- if (['audio', 'video'].includes(attachment.get('type'))) {
- content = (
-
- );
-
- if (attachment.get('type') === 'audio') {
- label =
;
- } else {
- label =
;
- }
- } else if (attachment.get('type') === 'image') {
- const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
- const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
- const x = ((focusX / 2) + .5) * 100;
- const y = ((focusY / -2) + .5) * 100;
-
- content = (
-
- );
- } else if (attachment.get('type') === 'gifv') {
- content = (
-
- );
-
- label = 'GIF';
- }
-
- thumbnail = (
-
- {content}
-
- {label && (
-
- {label}
-
- )}
-
- );
- }
-
- return (
-
- );
- }
-
-}
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.tsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.tsx
new file mode 100644
index 0000000000..6279d3c881
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.tsx
@@ -0,0 +1,200 @@
+import { useState, useCallback } from 'react';
+
+import classNames from 'classnames';
+
+import HeadphonesIcon from '@/material-icons/400-24px/headphones-fill.svg?react';
+import MovieIcon from '@/material-icons/400-24px/movie-fill.svg?react';
+import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
+import { Blurhash } from 'flavours/glitch/components/blurhash';
+import { Icon } from 'flavours/glitch/components/icon';
+import { formatTime } from 'flavours/glitch/features/video';
+import {
+ autoPlayGif,
+ displayMedia,
+ useBlurhash,
+} from 'flavours/glitch/initial_state';
+import type { Status, MediaAttachment } from 'flavours/glitch/models/status';
+
+export const MediaItem: React.FC<{
+ attachment: MediaAttachment;
+ onOpenMedia: (arg0: MediaAttachment) => void;
+}> = ({ attachment, onOpenMedia }) => {
+ const [visible, setVisible] = useState(
+ (displayMedia !== 'hide_all' &&
+ !attachment.getIn(['status', 'sensitive'])) ||
+ displayMedia === 'show_all',
+ );
+ const [loaded, setLoaded] = useState(false);
+
+ const handleImageLoad = useCallback(() => {
+ setLoaded(true);
+ }, [setLoaded]);
+
+ const handleMouseEnter = useCallback(
+ (e: React.MouseEvent
) => {
+ if (e.target instanceof HTMLVideoElement) {
+ void e.target.play();
+ }
+ },
+ [],
+ );
+
+ const handleMouseLeave = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.target instanceof HTMLVideoElement) {
+ e.target.pause();
+ e.target.currentTime = 0;
+ }
+ },
+ [],
+ );
+
+ const handleClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+
+ if (visible) {
+ onOpenMedia(attachment);
+ } else {
+ setVisible(true);
+ }
+ }
+ },
+ [attachment, visible, onOpenMedia, setVisible],
+ );
+
+ const status = attachment.get('status') as Status;
+ const description = (attachment.getIn(['translation', 'description']) ||
+ attachment.get('description')) as string | undefined;
+ const previewUrl = attachment.get('preview_url') as string;
+ const fullUrl = attachment.get('url') as string;
+ const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
+ const lang = status.get('language') as string;
+ const blurhash = attachment.get('blurhash') as string;
+ const statusUrl = status.get('url') as string;
+ const type = attachment.get('type') as string;
+
+ let thumbnail;
+
+ const badges = [];
+
+ if (description && description.length > 0) {
+ badges.push(
+
+ ALT
+ ,
+ );
+ }
+
+ if (!visible) {
+ thumbnail = (
+
+
+
+ );
+ } else if (type === 'audio') {
+ thumbnail = (
+ <>
+
+
+
+
+
+ >
+ );
+ } else if (type === 'image') {
+ const focusX = (attachment.getIn(['meta', 'focus', 'x']) || 0) as number;
+ const focusY = (attachment.getIn(['meta', 'focus', 'y']) || 0) as number;
+ const x = (focusX / 2 + 0.5) * 100;
+ const y = (focusY / -2 + 0.5) * 100;
+
+ thumbnail = (
+
+ );
+ } else if (['video', 'gifv'].includes(type)) {
+ const duration = attachment.getIn([
+ 'meta',
+ 'original',
+ 'duration',
+ ]) as number;
+
+ thumbnail = (
+
+
+
+ {type === 'video' && (
+
+
+
+ )}
+
+ );
+
+ if (type === 'gifv') {
+ badges.push(
+
+ GIF
+ ,
+ );
+ } else {
+ badges.push(
+
+ {formatTime(Math.floor(duration))}
+ ,
+ );
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.jsx b/app/javascript/flavours/glitch/features/account_gallery/index.jsx
index d3f845ddc4..284713f93d 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.jsx
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx
@@ -20,7 +20,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
-import MediaItem from './components/media_item';
+import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx
index 5a70707179..dcce9a47de 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx
@@ -195,6 +195,28 @@ export const DetailedStatus: React.FC<{
)
) {
media.push();
+ } else if (
+ ['image', 'gifv'].includes(
+ status.getIn(['media_attachments', 0, 'type']) as string,
+ ) ||
+ status.get('media_attachments').size > 1
+ ) {
+ media.push(
+ ,
+ );
+ mediaIcons.push('picture-o');
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description =
@@ -249,23 +271,6 @@ export const DetailedStatus: React.FC<{
/>,
);
mediaIcons.push('video-camera');
- } else {
- media.push(
- ,
- );
- mediaIcons.push('picture-o');
}
} else if (status.get('spoiler_text').length === 0) {
media.push(
diff --git a/app/javascript/flavours/glitch/models/status.ts b/app/javascript/flavours/glitch/models/status.ts
index bf1784bc61..9246f36213 100644
--- a/app/javascript/flavours/glitch/models/status.ts
+++ b/app/javascript/flavours/glitch/models/status.ts
@@ -10,3 +10,5 @@ export type Status = Immutable.Map;
type CardShape = Required;
export type Card = RecordOf;
+
+export type MediaAttachment = Immutable.Map;
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss
index 7d580f1af2..22505202a2 100644
--- a/app/javascript/flavours/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components.scss
@@ -242,6 +242,7 @@
flex: 0 0 auto;
a {
+ display: flex;
color: inherit;
text-decoration: none;
}
@@ -6287,6 +6288,7 @@ a.status-card {
.icon {
width: 24px;
height: 24px;
+ filter: var(--overlay-icon-shadow);
}
&:hover,
@@ -6381,6 +6383,10 @@ a.status-card {
.icon-button {
color: $white;
+ .icon {
+ filter: var(--overlay-icon-shadow);
+ }
+
&:hover,
&:focus,
&:active {
@@ -6439,6 +6445,7 @@ a.status-card {
.media-modal__page-dot {
flex: 0 0 auto;
background-color: $white;
+ filter: var(--overlay-icon-shadow);
opacity: 0.4;
height: 6px;
width: 6px;
@@ -7616,8 +7623,8 @@ img.modal-warning {
width: 100%;
min-height: 64px;
display: grid;
- grid-template-columns: 50% 50%;
- grid-template-rows: 50% 50%;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
gap: 2px;
@include fullwidth-gallery;
@@ -7630,6 +7637,9 @@ img.modal-warning {
position: relative;
border-radius: 8px;
overflow: hidden;
+ outline: 1px solid var(--media-outline-color);
+ outline-offset: -1px;
+ z-index: 1;
&--tall {
grid-row: span 2;
@@ -7646,15 +7656,44 @@ img.modal-warning {
&.letterbox {
background: $base-shadow-color;
}
+
+ &--square {
+ aspect-ratio: 1;
+ }
+
+ &__overlay {
+ position: absolute;
+ top: 0;
+ inset-inline-start: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ padding: 8px;
+ z-index: 1;
+
+ &--corner {
+ align-items: flex-start;
+ justify-content: flex-end;
+ }
+
+ .icon {
+ color: $white;
+ filter: var(--overlay-icon-shadow);
+ }
+ }
}
.media-gallery__item-thumbnail {
- cursor: zoom-in;
+ cursor: pointer;
display: block;
text-decoration: none;
color: $secondary-text-color;
position: relative;
- z-index: 1;
+ z-index: -1;
&,
img {
@@ -7676,7 +7715,7 @@ img.modal-warning {
position: absolute;
top: 0;
inset-inline-start: 0;
- z-index: 0;
+ z-index: -2;
background: $base-overlay-background;
&--hidden {
@@ -7689,10 +7728,11 @@ img.modal-warning {
overflow: hidden;
position: relative;
width: 100%;
+ z-index: -1;
}
.media-gallery__item-gifv-thumbnail {
- cursor: zoom-in;
+ cursor: pointer;
height: 100%;
width: 100%;
object-fit: contain;
@@ -7704,13 +7744,6 @@ img.modal-warning {
}
}
-.media-gallery__item-thumbnail-label {
- clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
- clip: rect(1px, 1px, 1px, 1px);
- overflow: hidden;
- position: absolute;
-}
-
/* End Media Gallery */
.detailed,
@@ -7733,6 +7766,8 @@ img.modal-warning {
border-radius: 8px;
padding-bottom: 44px;
width: 100%;
+ outline: 1px solid var(--media-outline-color);
+ outline-offset: -1px;
&.editable {
border-radius: 0;
@@ -7789,6 +7824,7 @@ img.modal-warning {
.video-player__controls {
padding-top: 10px;
background: transparent;
+ z-index: 1;
}
}
@@ -7802,16 +7838,15 @@ img.modal-warning {
color: $white;
display: flex;
align-items: center;
+ outline: 1px solid var(--media-outline-color);
+ outline-offset: -1px;
+ z-index: 2;
&.editable {
border-radius: 0;
height: 100% !important;
}
- &:focus {
- outline: 0;
- }
-
.detailed-status & {
width: 100%;
height: 100%;
@@ -7823,7 +7858,7 @@ img.modal-warning {
display: block;
max-width: 100vw;
max-height: 80vh;
- z-index: 1;
+ z-index: -2;
position: relative;
}
@@ -7850,7 +7885,7 @@ img.modal-warning {
&__controls {
position: absolute;
direction: ltr;
- z-index: 2;
+ z-index: -1;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
@@ -8165,26 +8200,16 @@ img.modal-warning {
}
.account-gallery__container {
- display: flex;
- flex-wrap: wrap;
- padding: 4px 2px;
-}
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 2px;
-.account-gallery__item {
- border: 0;
- box-sizing: border-box;
- display: block;
- position: relative;
- border-radius: 4px;
- overflow: hidden;
- margin: 2px;
+ .media-gallery__item {
+ border-radius: 0;
+ }
- &__icons {
- position: absolute;
- top: 50%;
- inset-inline-start: 50%;
- transform: translate(-50%, -50%);
- font-size: 24px;
+ .load-more {
+ grid-column: span 3;
}
}
diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss
index 6485b9e9b9..d19db06855 100644
--- a/app/javascript/flavours/glitch/styles/variables.scss
+++ b/app/javascript/flavours/glitch/styles/variables.scss
@@ -117,6 +117,8 @@ $dismiss-overlay-width: 4rem;
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
--on-surface-color: #{transparentize($ui-base-color, 0.5)};
--avatar-border-radius: 8px;
+ --media-outline-color: #{rgba(#fcf8ff, 0.15)};
+ --overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)});
--error-background-color: #{darken($error-red, 16%)};
--error-active-background-color: #{darken($error-red, 12%)};
--on-error-color: #fff;