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`;
+};