Stories: Implement Weather Widget (#5027)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2024-10-20 18:53:16 +02:00
parent 63e5b9529f
commit af3f767dcc
15 changed files with 242 additions and 6 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -7,6 +7,10 @@
height: var(--custom-emoji-size);
position: relative;
flex: 0 0 var(--custom-emoji-size);
:global(.rlottie-canvas) {
display: block;
}
}
.placeholder {

View File

@ -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();

View File

@ -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;
}

View File

@ -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 (
<MediaAreaWeather
// eslint-disable-next-line react/no-array-index-key
key={`${mediaArea.type}-${i}`}
mediaArea={mediaArea}
className={styles.mediaArea}
style={prepareStyle(mediaArea)}
isPreview={isActive}
/>
);
}
default:
return undefined;
}

View File

@ -82,12 +82,12 @@ const MediaAreaSuggestedReaction = ({
return (
<div
ref={ref}
className={buildClassName(styles.suggestedReaction, isDark && styles.dark, className)}
className={buildClassName(isDark ? styles.dark : styles.light, className)}
style={buildStyle(style, `--custom-emoji-size: ${customEmojiSize}px`)}
onClick={handleClick}
>
<div
className={buildClassName(styles.background, isFlipped && styles.flipped)}
className={buildClassName(styles.reactionBackground, isFlipped && styles.flipped)}
/>
{Boolean(customEmojiSize) && (
<ReactionAnimatedEmoji

View File

@ -0,0 +1,98 @@
import React, {
type FC, memo, useRef, useState,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { ApiMediaAreaWeather, ApiSticker } from '../../../api/types';
import { selectRestrictedEmoji } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { convertToRGBA, getTextColor } from '../../../util/colors';
import { formatTemperature } from '../../../util/formatTemperature';
import { REM } from '../../common/helpers/mediaDimensions';
import useLastCallback from '../../../hooks/useLastCallback';
import useResizeObserver from '../../../hooks/useResizeObserver';
import CustomEmoji from '../../common/CustomEmoji';
import styles from './MediaArea.module.scss';
type OwnProps = {
mediaArea: ApiMediaAreaWeather;
className?: string;
style?: string;
isPreview?: boolean;
};
type StateProps = {
restrictedEmoji?: ApiSticker;
};
const EMOJI_SIZE_MULTIPLIER = 0.7;
const TEMPERATURE_SIZE = 32;
const MediaAreaWeather: FC<OwnProps & StateProps> = ({
mediaArea,
className,
style,
restrictedEmoji,
isPreview,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(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 (
<div
ref={ref}
className={buildClassName(className, styles.withBackground, isPreview && styles.border)}
style={buildStyle(
style,
`--custom-emoji-size: ${customEmojiSize}px`,
`--custom-background-color: ${backgroundColor}`,
)}
>
<div className={styles.weatherInfo}>
{restrictedEmoji && (
<CustomEmoji
key={restrictedEmoji.id}
documentId={restrictedEmoji.id}
size={customEmojiSize}
noPlay={!isPreview}
withTranslucentThumb
forceAlways
/>
)}
<p
className={styles.temperature}
style={buildStyle(`font-size: ${customTemperatureSize}rem`, `color: ${textColor}`)}
>
{formatTemperature(temperatureC)}
</p>
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>((global, ownProps): StateProps => {
const { mediaArea } = ownProps;
const restrictedEmoji = selectRestrictedEmoji(global, mediaArea.emoji);
return { restrictedEmoji };
})(MediaAreaWeather));

View File

@ -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';

View File

@ -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<void> =>
setGlobal(global);
});
addActionHandler('loadRestrictedEmojiStickers', async (global): Promise<void> => {
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<void> => {
const stickerSet = await callApi('fetchGenericEmojiEffects');
if (!stickerSet) {

View File

@ -133,6 +133,21 @@ export function selectAnimatedEmoji<T extends GlobalState>(global: T, emoji: str
return animatedEmojis.stickers.find((sticker) => sticker.emoji === emoji || sticker.emoji === cleanedEmoji);
}
export function selectRestrictedEmoji<T extends GlobalState>(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<T extends GlobalState>(global: T, emoji: string) {
const { animatedEmojiEffects } = global;
if (!animatedEmojiEffects || !animatedEmojiEffects.stickers) {

View File

@ -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;

View File

@ -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<string, RLottie>();
@ -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`;

View File

@ -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';
};

View File

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