diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts index a369b55ed..5dd1a338e 100644 --- a/src/api/gramjs/apiBuilders/stories.ts +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -233,6 +233,20 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un }; } + if (area instanceof GramJs.MediaAreaWeather) { + const { + coordinates, emoji, temperatureC, color, + } = area; + + return { + type: 'weather', + coordinates: buildApiMediaAreaCoordinates(coordinates), + emoji, + temperatureC, + color, + }; + } + return undefined; } diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index fa069b85b..0fdd0a853 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -168,5 +168,13 @@ export type ApiMediaAreaUrl = { url: string; }; +export type ApiMediaAreaWeather = { + type: 'weather'; + coordinates: ApiMediaAreaCoordinates; + emoji: string; + temperatureC: number; + color: number; +}; + export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction -| ApiMediaAreaChannelPost | ApiMediaAreaUrl; +| ApiMediaAreaChannelPost | ApiMediaAreaUrl | ApiMediaAreaWeather; diff --git a/src/components/common/CustomEmoji.module.scss b/src/components/common/CustomEmoji.module.scss index 18b1d5571..e12a7f818 100644 --- a/src/components/common/CustomEmoji.module.scss +++ b/src/components/common/CustomEmoji.module.scss @@ -7,6 +7,10 @@ height: var(--custom-emoji-size); position: relative; flex: 0 0 var(--custom-emoji-size); + + :global(.rlottie-canvas) { + display: block; + } } .placeholder { diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index ae4ba737e..38a2396ac 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -203,6 +203,7 @@ const Main = ({ initMain, loadAnimatedEmojis, loadBirthdayNumbersStickers, + loadRestrictedEmojiStickers, loadNotificationSettings, loadNotificationExceptions, updateIsOnline, @@ -327,6 +328,7 @@ const Main = ({ loadPremiumGifts(); loadAvailableEffects(); loadBirthdayNumbersStickers(); + loadRestrictedEmojiStickers(); loadGenericEmojiEffects(); loadSavedReactionTags(); loadAuthorizations(); diff --git a/src/components/story/mediaArea/MediaArea.module.scss b/src/components/story/mediaArea/MediaArea.module.scss index 08dc1b200..e91df5c06 100644 --- a/src/components/story/mediaArea/MediaArea.module.scss +++ b/src/components/story/mediaArea/MediaArea.module.scss @@ -57,7 +57,7 @@ } } -.suggestedReaction { +.light { --background-color: white; color: black; } @@ -67,7 +67,7 @@ color: white; } -.background { +.reactionBackground { width: 100%; height: 100%; background-color: var(--background-color); @@ -114,6 +114,13 @@ } } +.withBackground { + width: 100%; + height: 100%; + background-color: var(--custom-background-color); + filter: drop-shadow(0 0.125rem 0.25rem var(--color-default-shadow)); +} + .reaction { position: absolute; top: 50%; @@ -133,3 +140,27 @@ transform: translateX(-50%); font-weight: 500; } + +.weatherInfo { + width: 100%; + height: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transition: transform 200ms ease-out; + display: flex; + align-items: center; + justify-content: center; + gap: 0.3125rem; + white-space: nowrap; +} + +.temperature { + font-weight: 700; + margin: 0; +} + +.border { + border-radius: 0.3125rem !important; +} diff --git a/src/components/story/mediaArea/MediaAreaOverlay.tsx b/src/components/story/mediaArea/MediaAreaOverlay.tsx index e8819a9dd..89066d900 100644 --- a/src/components/story/mediaArea/MediaAreaOverlay.tsx +++ b/src/components/story/mediaArea/MediaAreaOverlay.tsx @@ -11,6 +11,7 @@ import buildStyle from '../../../util/buildStyle'; import useWindowSize from '../../../hooks/window/useWindowSize'; import MediaAreaSuggestedReaction from './MediaAreaSuggestedReaction'; +import MediaAreaWeather from './MediaAreaWeather'; import styles from './MediaArea.module.scss'; @@ -116,6 +117,18 @@ const MediaAreaOverlay = ({ style={prepareStyle(mediaArea)} /> ); + case 'weather': { + return ( + + ); + } default: return undefined; } diff --git a/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx b/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx index 8bd31bea7..cc15e595e 100644 --- a/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx +++ b/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx @@ -82,12 +82,12 @@ const MediaAreaSuggestedReaction = ({ return (
{Boolean(customEmojiSize) && ( = ({ + mediaArea, + className, + style, + restrictedEmoji, + isPreview, +}) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const [customEmojiSize, setCustomEmojiSize] = useState(1.5 * REM); + const [customTemperatureSize, setCustomTemperatureSize] = useState(0); + + const { temperatureC, color } = mediaArea; + + const backgroundColor = convertToRGBA(color); + const textColor = getTextColor(color); + + const updateCustomSize = useLastCallback(() => { + if (!ref.current) return; + const height = ref.current.clientHeight; + setCustomEmojiSize(Math.round(height * EMOJI_SIZE_MULTIPLIER)); + setCustomTemperatureSize(height / TEMPERATURE_SIZE); + }); + + useResizeObserver(ref, updateCustomSize); + + return ( +
+
+ {restrictedEmoji && ( + + )} +

+ {formatTemperature(temperatureC)} +

+
+
+ ); +}; + +export default memo(withGlobal((global, ownProps): StateProps => { + const { mediaArea } = ownProps; + const restrictedEmoji = selectRestrictedEmoji(global, mediaArea.emoji); + return { restrictedEmoji }; +})(MediaAreaWeather)); diff --git a/src/config.ts b/src/config.ts index 53a3daa5e..34aa76496 100644 --- a/src/config.ts +++ b/src/config.ts @@ -222,6 +222,7 @@ export const MENU_TRANSITION_DURATION = 200; export const SLIDE_TRANSITION_DURATION = 450; export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji'; +export const RESTRICTED_EMOJI_SET = 'RestrictedEmoji'; export const VIDEO_WEBM_TYPE = 'video/webm'; export const GIF_MIME_TYPE = 'image/gif'; diff --git a/src/global/actions/api/symbols.ts b/src/global/actions/api/symbols.ts index 10fcee655..69d860ebd 100644 --- a/src/global/actions/api/symbols.ts +++ b/src/global/actions/api/symbols.ts @@ -4,7 +4,7 @@ import type { import type { RequiredGlobalActions } from '../../index'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; -import { BIRTHDAY_NUMBERS_SET } from '../../../config'; +import { BIRTHDAY_NUMBERS_SET, RESTRICTED_EMOJI_SET } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey } from '../../../util/iteratees'; import { oldTranslate } from '../../../util/oldLangProvider'; @@ -286,6 +286,26 @@ addActionHandler('loadBirthdayNumbersStickers', async (global): Promise => setGlobal(global); }); +addActionHandler('loadRestrictedEmojiStickers', async (global): Promise => { + const emojis = await callApi('fetchStickers', { + stickerSetInfo: { + shortName: RESTRICTED_EMOJI_SET, + }, + }); + if (!emojis) { + return; + } + + global = getGlobal(); + + global = { + ...global, + restrictedEmoji: { ...emojis.set, stickers: emojis.stickers }, + }; + + setGlobal(global); +}); + addActionHandler('loadGenericEmojiEffects', async (global): Promise => { const stickerSet = await callApi('fetchGenericEmojiEffects'); if (!stickerSet) { diff --git a/src/global/selectors/symbols.ts b/src/global/selectors/symbols.ts index 3a37c7091..86646ba1e 100644 --- a/src/global/selectors/symbols.ts +++ b/src/global/selectors/symbols.ts @@ -133,6 +133,21 @@ export function selectAnimatedEmoji(global: T, emoji: str return animatedEmojis.stickers.find((sticker) => sticker.emoji === emoji || sticker.emoji === cleanedEmoji); } +export function selectRestrictedEmoji(global: T, emoji: string) { + const { restrictedEmoji } = global; + if (!restrictedEmoji || !restrictedEmoji.stickers) { + return undefined; + } + + const cleanedEmoji = cleanEmoji(emoji); + + return restrictedEmoji.stickers.find((sticker) => { + if (!sticker.emoji) return undefined; + const cleanedStickerEmoji = cleanEmoji(sticker.emoji); + return cleanedStickerEmoji === cleanedEmoji; + }); +} + export function selectAnimatedEmojiEffect(global: T, emoji: string) { const { animatedEmojiEffects } = global; if (!animatedEmojiEffects || !animatedEmojiEffects.stickers) { diff --git a/src/global/types.ts b/src/global/types.ts index 2df9a3952..d9e084751 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1150,6 +1150,7 @@ export type GlobalState = { animatedEmojiEffects?: ApiStickerSet; genericEmojiEffects?: ApiStickerSet; birthdayNumbers?: ApiStickerSet; + restrictedEmoji?: ApiStickerSet; defaultTopicIconsId?: string; defaultStatusIconsId?: string; premiumGifts?: ApiStickerSet; @@ -2892,6 +2893,7 @@ export interface ActionPayloads { loadGreetingStickers: undefined; loadGenericEmojiEffects: undefined; loadBirthdayNumbersStickers: undefined; + loadRestrictedEmojiStickers: undefined; loadAvailableEffects: undefined; diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index f61a7c5e9..f0cc9c803 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -27,6 +27,7 @@ const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75; const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24; const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4; const LOW_PRIORITY_CACHE_MODULO = 0; +const CANVAS_CLASS = 'rlottie-canvas'; const workers = launchMediaWorkers().map(({ connector }) => connector); const instancesByRenderId = new Map(); @@ -281,6 +282,8 @@ class RLottie { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d')!; + canvas.classList.add(CANVAS_CLASS); + canvas.style.width = `${size}px`; canvas.style.height = `${size}px`; diff --git a/src/util/colors.ts b/src/util/colors.ts index 0d84b2cf0..4e98496eb 100644 --- a/src/util/colors.ts +++ b/src/util/colors.ts @@ -7,6 +7,8 @@ import { preloadImage } from './files'; +const LUMA_THRESHOLD = 128; + /** * HEX > RGB * input: 'xxxxxx' (ex. 'ed15fa') case-insensitive @@ -202,3 +204,22 @@ export function getPatternColor(rgbColor: [number, number, number]) { return `hsla(${hue * 360}, ${saturation * 100}%, ${value * 100}%, .4)`; } + +/* eslint-disable no-bitwise */ +export const convertToRGBA = (color: number): string => { + const alpha = (color >> 24) & 0xff; + const red = (color >> 16) & 0xff; + const green = (color >> 8) & 0xff; + const blue = color & 0xff; + + const alphaFloat = alpha / 255; + return `rgba(${red}, ${green}, ${blue}, ${alphaFloat})`; +}; + +export const getTextColor = (color: number): string => { + const r = (color >> 16) & 0xff; + const g = (color >> 8) & 0xff; + const b = color & 0xff; + const luma = getColorLuma([r, g, b]); + return luma > LUMA_THRESHOLD ? 'black' : 'white'; +}; diff --git a/src/util/formatTemperature.ts b/src/util/formatTemperature.ts new file mode 100644 index 000000000..6c1fa6505 --- /dev/null +++ b/src/util/formatTemperature.ts @@ -0,0 +1,4 @@ +export const formatTemperature = (temperatureC: number) => { + const isFahrenheit = Boolean(navigator.language === 'en-US'); + return isFahrenheit ? `${Math.round((temperatureC * 9) / 5 + 32)} °F` : `${Math.round(temperatureC)} °C`; +};