Files
discord-decomp/modules/icymi/native/media/ICYMIMediaMosaic.tsx
T
2026-11-01 12:12:55 +02:00

492 lines
20 KiB
TypeScript

// Metro module 15367 (f15368).
import _slicedToArray from "../../../../_bundle/_slicedToArray";
import { useState, useMemo, useRef, useCallback, useContext } from "react";
import { Pressable, View } from "react-native";
import AccessibilityStore from "../../../a11y/AccessibilityStore";
import ChannelStore from "../../../../stores/ChannelStore";
import RelationshipStore from "../../../../stores/RelationshipStore";
import UserStore from "../../../../stores/UserStore";
import ICYMIStore from "../../ICYMIStore";
import { AnalyticsObjectTypes, AnalyticsObjects, AnalyticsPages } from "../../../../Constants";
import { createStyles } from "../../../../design/components/Styles/native/createStyles";
import Module712 from "../../../../_bundle/Module712";
import { urlMatchesFileExtension } from "../../../messages/MediaFormatTesters";
import { GIF_RE_IOS } from "../../../../ConstantsIOS";
import { useStateFromStores } from "../../../../_bundle/Module589";
import Video from "../../../../components_native/common/Video";
import ReanimatedRexport, { useAnimatedStyle } from "../../../reanimated/ReanimatedRexport";
import * as _timing from "../../../../design/animation/reanimated/timing/timing";
import { createThumbhashImageFromPlaceholder } from "../../../thumbhash/ThumbhashUtils";
import FastImage from "../../../../components_native/common/FastImage";
import { Text } from "../../../../design/components/Text/native/Text";
import { intl, t } from "../../../../intl/index.native";
import { PlayIcon } from "../../../../design/components/Icon/native/redesign/generated/PlayIcon";
import { isVideo } from "../../../../utils/native/UploadUtils";
import { PressableOpacity } from "../../../../design/void/Pressables/native/Pressables";
import ICYMIActionCreators from "../../ICYMIActionCreators";
import { VoiceXIcon } from "../../../../design/components/Icon/native/redesign/generated/VoiceXIcon";
import { VoiceNormalIcon } from "../../../../design/components/Icon/native/redesign/generated/VoiceNormalIcon";
import { ICYMIContext } from "../ICYMIContext";
import { extractMediaSourcesFromMessage, flattenSource } from "../../../media_viewer/native/MediaSourceUtil";
import { GRAVITY_VALID_EMBED_TYPES } from "../../ICYMITypes";
import { isNotNullish } from "../../../../utils/GlobalUtils";
import { partition } from "../../../../_bundle/Module12";
import { ack } from "../../../../actions/ReadStateActionCreators";
import { openMediaModal } from "../../../media_viewer/native/components/openMediaModal";
import { computeChannelName } from "../../../channel/useChannelName";
let useStyles, __initData, MediaMosaicImage, Media, OneImageRow, ThreeImagesRow, TwoImagesRow, ThreeImages, FourImages, SingleImage, GravityAttachmentMediaMosaic;
const MediaMosaicVideo = function MediaMosaicVideo(props) {
const { source: source3, height, width: width2, autoplay, style } = props;
const styles = useStyles();
let muted = useStateFromStores([ICYMIStore], () => ICYMIStore.videosMuted());
let videoURI = source3.videoURI;
if (videoURI == null) {
videoURI = source3.sourceURI;
}
if (videoURI == null) {
videoURI = source3.uri;
}
const props2 = {
src: {
videoURI
},
height,
width: width2,
postponeRender: false,
paused: !autoplay,
muted,
resizeMode: "cover",
style: [styles.media, style],
videoStyle: styles.video,
disableFocus: null
};
if (!muted) {
muted = source3.isGIFV;
}
props2.disableFocus = muted;
return <Video {...props2} />;
};
MediaMosaicImage = function MediaMosaicImage(props) {
const { source, dimensions: dimensions2, style, isSpoiler } = props;
const styles = useStyles();
const [state, setState] = useState(false);
const c = function c() {
return {
opacity: _timing.withTiming(state ? 0 : 1, {
duration: 150
})
};
};
c.__closure = {
withTiming: _timing.withTiming,
imageFinishedLoading: state
};
c.__workletHash = 7803531897566;
c.__initData = __initData;
return <>
{[<ReanimatedRexport.View style={[useAnimatedStyle(c), styles.thumbhashMedia]}>
<FastImage source={useMemo(() => {
if (source.placeholder != null) {
return {
uri: createThumbhashImageFromPlaceholder(source.placeholder),
width: source.width,
height: source.height
};
}
}, [source.height, source.placeholder, source.width])} style={[style, styles.media, dimensions2]} />
</ReanimatedRexport.View>, <ReanimatedRexport.Image key={source.uri} source={source} style={[styles.media, style, dimensions2]} onLoadEnd={() => setState(true)} blurRadius={isSpoiler ? 100 : 0} />]}
</>;
};
Media = function Media(props) {
let stateFromStores;
const { source: source3, dimensions: dimensions2, initialIndex, handlePressMedia, style, visible: visible2 } = props;
stateFromStores = void 0;
const styles = useStyles();
const ref = useRef(null);
let spoiler = source3.spoiler;
if (spoiler == null) {
spoiler = false;
}
const [state2, setState2] = useState(spoiler);
const callback2 = useCallback(() => {
if (state2) {
setState2(false);
} else {
handlePressMedia({
ref,
initialIndex
});
}
}, [handlePressMedia, initialIndex, state2]);
stateFromStores = useStateFromStores([ICYMIStore], () => ICYMIStore.videosMuted());
const stateFromStores3 = useStateFromStores([AccessibilityStore], () => AccessibilityStore.useReducedMotion);
const r72 = [state2 ? <View style={styles.centerContainer}>
<View style={styles.spoilerText}>
<Text maxFontSizeMultiplier={1} variant="heading-md/semibold" color="mobile-text-heading-primary">
{intl.string(t["F+x38C"]).toUpperCase()}
</Text>
</View>
</View> : state2, source3.videoURI != null && !state2 && !(source3.isGIFV || urlMatchesFileExtension(source3.sourceURI, GIF_RE_IOS)) && stateFromStores3 && <View style={styles.centerContainer}>
<View style={styles.videoIcon}>
<PlayIcon color={Module712.colors.REDESIGN_BUTTON_TERTIARY_TEXT} size="lg" />
</View>
</View>, isVideo(source3.uri) ? source3.isGIFV || urlMatchesFileExtension(source3.sourceURI, GIF_RE_IOS) ? null : <View style={styles.absoluteContainer}>
<PressableOpacity style={[styles.muteIcon, stateFromStores ? styles.iconBg : styles.iconBgSelected]} onPress={() => ICYMIActionCreators.setVideosMuted(!stateFromStores)} activeOpacity={0.8}>
{stateFromStores ? <VoiceXIcon color={Module712.colors.INTERACTIVE_TEXT_DEFAULT} size="sm" /> : <VoiceNormalIcon color={Module712.colors.BLACK} size="sm" />}
</PressableOpacity>
</View> : null, !(!isVideo(source3.uri) && !(source3.isGIFV || urlMatchesFileExtension(source3.sourceURI, GIF_RE_IOS))) && source3.videoURI != null ? <MediaMosaicVideo source={source3} height={dimensions2.height} width={dimensions2.width} style={style} autoplay={!!visible2} /> : <MediaMosaicImage source={source3} style={style} dimensions={dimensions2} isSpoiler={state2} />];
return <Pressable ref={ref} onPress={callback2} style={dimensions2}>{r72}</Pressable>;
};
OneImageRow = function OneImageRow(props) {
const { source: source3, handlePressMedia: handlePressMedia5 } = props;
let widthOverride3 = props.widthOverride;
const styles = useStyles();
const context = useContext(ICYMIContext);
if (widthOverride3 == null) {
widthOverride3 = context?.width - context.inset - 2 * context.margin;
}
return <View style={[styles.imageRow, styles.topRow]}>
<Media handlePressMedia={handlePressMedia5} initialIndex={0} source={source3} dimensions={{
width: widthOverride3,
height: widthOverride3 / 1.5
}} />
</View>;
};
ThreeImagesRow = function ThreeImagesRow(props) {
const sources2 = props.sources;
let start = props.start;
let end = props.end;
const { offset, handlePressMedia: handlePressMedia2 } = props;
let widthOverride3 = props.widthOverride;
const styles = useStyles();
const context = useContext(ICYMIContext);
if (widthOverride3 == null) {
widthOverride3 = context?.width - context.inset - 2 * context.margin;
}
let c21917_2 = widthOverride3;
const style = [styles.imageRow, , ,];
if (start) {
start = styles.topRow;
}
style[1] = start;
if (end) {
end = styles.bottomRow;
}
style[2] = end;
return <View style={style}>
{sources2.map((item, index) => <Media key={offset + index} handlePressMedia={handlePressMedia2} initialIndex={offset + index} source={item} dimensions={{
width: (c21917_2 - 8) / 3,
height: (c21917_2 - 8) / 3
}} />)}
</View>;
};
TwoImagesRow = function TwoImagesRow(props) {
const { sources: sources2, handlePressMedia: handlePressMedia3 } = props;
let end = props.end;
let widthOverride3 = props.widthOverride;
const styles = useStyles();
const context = useContext(ICYMIContext);
if (widthOverride3 == null) {
widthOverride3 = context?.width - context.inset - 2 * context.margin;
}
const dimensions = {
width: (widthOverride3 - 4) / 2,
height: (widthOverride3 - 4) / 2 / 0.75
};
const style = [styles.imageRow, styles.topRow, ,];
if (end) {
end = styles.bottomRow;
}
style[2] = end;
return <View style={style}>
{sources2.map((item, index) => <Media key={index} handlePressMedia={handlePressMedia3} initialIndex={index} source={item} dimensions={dimensions} />)}
</View>;
};
ThreeImages = function ThreeImages(props) {
const { sources: sources2, handlePressMedia: handlePressMedia5 } = props;
let widthOverride3 = props.widthOverride;
const styles = useStyles();
const context = useContext(ICYMIContext);
if (widthOverride3 == null) {
widthOverride3 = context?.width - context.inset - 2 * context.margin;
}
return <View style={[styles.imagesContainer, styles.imageRow]}>
<View style={styles.leftColumn}>
<Media handlePressMedia={handlePressMedia5} initialIndex={0} source={sources2[0]} dimensions={{
width: 2 * widthOverride3 / 3 - 4,
height: 2 * widthOverride3 / 3
}} />
</View>
<View style={styles.rightColumn}>
<Media handlePressMedia={handlePressMedia5} initialIndex={1} source={sources2[1]} dimensions={{
width: widthOverride3 / 3,
height: widthOverride3 / 3
}} />
<Media handlePressMedia={handlePressMedia5} initialIndex={2} source={sources2[2]} dimensions={{
width: widthOverride3 / 3,
height: widthOverride3 / 3
}} />
</View>
</View>;
};
FourImages = function FourImages(props) {
const { sources: sources2, handlePressMedia: handlePressMedia5 } = props;
let widthOverride3 = props.widthOverride;
const styles = useStyles();
const context = useContext(ICYMIContext);
if (widthOverride3 == null) {
widthOverride3 = context?.width - context.inset - 2 * context.margin;
}
const dimensions2 = {
width: widthOverride3 / 2 - 4,
height: (widthOverride3 / 2 - 4) / 1.5
};
return <View style={styles.imagesContainer}>
<View style={[styles.imageRow, styles.topRow]}>
<Media handlePressMedia={handlePressMedia5} initialIndex={0} source={sources2[0]} dimensions={dimensions2} />
<Media handlePressMedia={handlePressMedia5} initialIndex={1} source={sources2[1]} dimensions={dimensions2} />
</View>
<View style={[styles.imageRow, styles.bottomRow]}>
<Media handlePressMedia={handlePressMedia5} initialIndex={2} source={sources2[2]} dimensions={dimensions2} />
<Media handlePressMedia={handlePressMedia5} initialIndex={3} source={sources2[3]} dimensions={dimensions2} />
</View>
</View>;
};
SingleImage = function SingleImage(props) {
const { source: source2, initialIndex: initialIndex2, handlePressMedia: handlePressMedia5, visible: visible2 } = props;
let widthOverride3 = props.widthOverride;
const styles = useStyles();
const context = useContext(ICYMIContext);
if (widthOverride3 == null) {
widthOverride3 = context?.width - context.inset - 2 * context.margin;
}
const width = widthOverride3;
return <View style={styles.imagesContainer}>
<Media handlePressMedia={handlePressMedia5} initialIndex={initialIndex2} source={source2} dimensions={useMemo(() => {
const r8 = source2.width / source2.height;
if (r8 >= 1) {
const width3 = Math.min(source2.width, width);
return {
height: width3 / r8,
width: width3
};
}
const height = Math.min(source2.height, 330);
const width2 = height * r8;
return width2 > width ? {
width,
height: width / r8
} : {
width: width2,
height
};
}, [source2.width, source2.height, width])} style={styles.singleImage} visible={visible2} />
</View>;
};
GravityAttachmentMediaMosaic = function GravityAttachmentMediaMosaic(props) {
const { sources, handlePressMedia: handlePressMedia4, visible: visible2, widthOverride } = props;
const length = sources.length;
const styles = useStyles();
const memo = useMemo(() => {
const r3 = length % 3;
let r0 = r3 !== 0 ? r3 : 3;
const r4 = [];
r4.push(sources.slice(0, r0));
while (r0 < length) {
const r15 = r0 + 3;
r4.push(sources.slice(r0, r15));
r0 = r15;
}
return r4;
}, [length, sources]);
return length !== 0 ? length === 1 ? <SingleImage widthOverride={widthOverride} initialIndex={0} handlePressMedia={handlePressMedia4} source={sources[0]} visible={visible2} /> : length === 3 ? <ThreeImages widthOverride={widthOverride} handlePressMedia={handlePressMedia4} sources={sources} /> : length === 4 ? <FourImages widthOverride={widthOverride} handlePressMedia={handlePressMedia4} sources={sources} /> : <View style={styles.imagesContainer}>
{memo.map((item, index) => {
if (item.length === 1) {
return <OneImageRow key={index} handlePressMedia={handlePressMedia4} source={item[0]} />;
}
if (item.length === 2) {
return <TwoImagesRow key={index} widthOverride={widthOverride} sources={item} handlePressMedia={handlePressMedia4} end={index === memo.length - 1} />;
}
let offset2 = 0;
const start = index === 0;
if (!start) {
offset2 = memo[0].length + 3 * (index - 1);
}
return <ThreeImagesRow key={index} widthOverride={widthOverride} handlePressMedia={handlePressMedia4} offset={offset2} sources={item} start={start} end={index === memo.length - 1} />;
})}
</View> : null;
};
useStyles = createStyles(() => ({
media: {
borderRadius: Module712.radii.xs
},
video: {
backgroundColor: Module712.colors.BACKGROUND_SURFACE_HIGH
},
thumbhashMedia: {
position: "absolute",
top: 0,
left: 0,
zIndex: 1
},
container: {
gap: 4
},
imagesContainer: {
justifyContent: "center",
gap: 4,
width: "100%"
},
imageRow: {
flexDirection: "row",
gap: 4
},
topRow: {
overflow: "hidden",
borderTopEndRadius: Module712.radii.lg,
borderTopStartRadius: Module712.radii.lg
},
bottomRow: {
overflow: "hidden",
borderBottomEndRadius: Module712.radii.lg,
borderBottomStartRadius: Module712.radii.lg
},
videoIcon: {
backgroundColor: Module712.colors.BACKGROUND_BASE_LOW,
borderRadius: Module712.radii.round,
padding: 16
},
muteIcon: {
position: "absolute",
borderRadius: Module712.radii.round,
padding: Module712.space.PX_4,
bottom: 8,
right: 8
},
spoilerText: {
backgroundColor: Module712.colors.SPOILER_HIDDEN_BACKGROUND,
borderRadius: Module712.radii.lg,
paddingHorizontal: Module712.space.PX_12,
paddingVertical: 6
},
leftColumn: {
overflow: "hidden",
borderTopStartRadius: Module712.radii.lg,
borderBottomStartRadius: Module712.radii.lg
},
rightColumn: {
overflow: "hidden",
borderTopEndRadius: Module712.radii.lg,
borderBottomEndRadius: Module712.radii.lg,
gap: 4
},
singleImage: {
overflow: "hidden",
borderRadius: Module712.radii.lg
},
centerContainer: {
position: "absolute",
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
zIndex: 2
},
absoluteContainer: {
position: "absolute",
width: "100%",
height: "100%",
zIndex: 2
},
iconBg: {
backgroundColor: Module712.colors.BACKGROUND_BASE_LOWER
},
iconBgSelected: {
backgroundColor: Module712.colors.WHITE
}
}));
__initData = {
code: "function ICYMIMediaMosaicTsx1(){const{withTiming,imageFinishedLoading}=this.__closure;return{opacity:withTiming(imageFinishedLoading?0:1,{duration:150})};}"
};
export default function ICYMIMediaMosaic(props) {
let r45;
let setState3, visible, allMediaSources, nonEmbedSources, callback;
const { message, widthOverride: widthOverride2, visible: visible2, itemType } = props;
setState3 = void 0;
visible = void 0;
allMediaSources = void 0;
nonEmbedSources = void 0;
callback = void 0;
const styles = useStyles();
const stateFromStores2 = useStateFromStores([ChannelStore], () => ChannelStore.getChannel(message.getChannelId()));
let state;
[state, setState3] = useState(false);
const r13 = state ? !state : visible2;
L7: {
visible = r13;
const memo2 = useMemo(() => {
const allMediaSources2 = extractMediaSourcesFromMessage(message, message, void 0, GRAVITY_VALID_EMBED_TYPES).map((item) => flattenSource(item)).filter(isNotNullish);
const r32 = _slicedToArray(partition(allMediaSources2, (a0) => a0.accessoryType === "embed"), 2);
return {
allMediaSources: allMediaSources2,
nonEmbedSources: r32[1],
embedSources: r32[0]
};
}, [message]);
allMediaSources = memo2.allMediaSources;
nonEmbedSources = memo2.nonEmbedSources;
const embedSources = memo2.embedSources;
callback = useCallback(({ ref: ref2, initialIndex: initialIndex2 }) => {
ICYMIActionCreators.itemInteracted(message.id, "message", "press_media");
ICYMIActionCreators.feedItemActioned({
itemId: message.id,
itemType,
actionParameters: {
actionGestureType: "press",
actionTargetElement: "media_mosaic",
actionIntentType: "open",
actionDestinationType: null
}
});
ack(message.channel_id, {
page: AnalyticsPages.ICYMI,
object: AnalyticsObjects.ACK_MEDIA_VIEWED,
objectType: AnalyticsObjectTypes.ACK_SEMI_AUTOMATIC
}, true, true, message.id);
const initialSources = [];
let c28009_0 = initialSources;
allMediaSources.forEach((allMediaSource) => {
c28009_0.push({
...allMediaSource,
embedURI: void 0
});
});
setState3(true);
openMediaModal({
disableDownload: false,
initialSources,
initialIndex: initialIndex2,
analyticsSource: "Channel",
channelId: message.channel_id,
contextName: stateFromStores2 != null ? computeChannelName(stateFromStores2, UserStore, RelationshipStore) : "",
contextIcon: void 0,
originViewOrOriginLayout: ref2.current,
onClose: () => setState3(false)
});
}, [message.channel_id, message.id, allMediaSources, stateFromStores2, itemType]);
if (nonEmbedSources.length === 0) {
const length2 = embedSources.length;
r45 = null;
if (length2 === 0) {
break L7;
}
}
r45 = <View key={`message-image-${message.id}`} style={styles.container}>
<GravityAttachmentMediaMosaic widthOverride={widthOverride2} sources={nonEmbedSources} handlePressMedia={callback} visible={visible} />
{embedSources == null ? void 0 : embedSources.map((embedSource, index) => <SingleImage key={`gif-${index}`} widthOverride={widthOverride2} handlePressMedia={callback} initialIndex={index + nonEmbedSources.length} source={embedSource} visible={visible} />)}
</View>;
}
return r45;
};
export { GravityAttachmentMediaMosaic };