Stories: Implement Weather Widget (#5027)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
parent
63e5b9529f
commit
af3f767dcc
@ -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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -168,5 +168,13 @@ export type ApiMediaAreaUrl = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiMediaAreaWeather = {
|
||||||
|
type: 'weather';
|
||||||
|
coordinates: ApiMediaAreaCoordinates;
|
||||||
|
emoji: string;
|
||||||
|
temperatureC: number;
|
||||||
|
color: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction
|
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction
|
||||||
| ApiMediaAreaChannelPost | ApiMediaAreaUrl;
|
| ApiMediaAreaChannelPost | ApiMediaAreaUrl | ApiMediaAreaWeather;
|
||||||
|
|||||||
@ -7,6 +7,10 @@
|
|||||||
height: var(--custom-emoji-size);
|
height: var(--custom-emoji-size);
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 var(--custom-emoji-size);
|
flex: 0 0 var(--custom-emoji-size);
|
||||||
|
|
||||||
|
:global(.rlottie-canvas) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
|
|||||||
@ -203,6 +203,7 @@ const Main = ({
|
|||||||
initMain,
|
initMain,
|
||||||
loadAnimatedEmojis,
|
loadAnimatedEmojis,
|
||||||
loadBirthdayNumbersStickers,
|
loadBirthdayNumbersStickers,
|
||||||
|
loadRestrictedEmojiStickers,
|
||||||
loadNotificationSettings,
|
loadNotificationSettings,
|
||||||
loadNotificationExceptions,
|
loadNotificationExceptions,
|
||||||
updateIsOnline,
|
updateIsOnline,
|
||||||
@ -327,6 +328,7 @@ const Main = ({
|
|||||||
loadPremiumGifts();
|
loadPremiumGifts();
|
||||||
loadAvailableEffects();
|
loadAvailableEffects();
|
||||||
loadBirthdayNumbersStickers();
|
loadBirthdayNumbersStickers();
|
||||||
|
loadRestrictedEmojiStickers();
|
||||||
loadGenericEmojiEffects();
|
loadGenericEmojiEffects();
|
||||||
loadSavedReactionTags();
|
loadSavedReactionTags();
|
||||||
loadAuthorizations();
|
loadAuthorizations();
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestedReaction {
|
.light {
|
||||||
--background-color: white;
|
--background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background {
|
.reactionBackground {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--background-color);
|
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 {
|
.reaction {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@ -133,3 +140,27 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
font-weight: 500;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import buildStyle from '../../../util/buildStyle';
|
|||||||
import useWindowSize from '../../../hooks/window/useWindowSize';
|
import useWindowSize from '../../../hooks/window/useWindowSize';
|
||||||
|
|
||||||
import MediaAreaSuggestedReaction from './MediaAreaSuggestedReaction';
|
import MediaAreaSuggestedReaction from './MediaAreaSuggestedReaction';
|
||||||
|
import MediaAreaWeather from './MediaAreaWeather';
|
||||||
|
|
||||||
import styles from './MediaArea.module.scss';
|
import styles from './MediaArea.module.scss';
|
||||||
|
|
||||||
@ -116,6 +117,18 @@ const MediaAreaOverlay = ({
|
|||||||
style={prepareStyle(mediaArea)}
|
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:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,12 +82,12 @@ const MediaAreaSuggestedReaction = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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`)}
|
style={buildStyle(style, `--custom-emoji-size: ${customEmojiSize}px`)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={buildClassName(styles.background, isFlipped && styles.flipped)}
|
className={buildClassName(styles.reactionBackground, isFlipped && styles.flipped)}
|
||||||
/>
|
/>
|
||||||
{Boolean(customEmojiSize) && (
|
{Boolean(customEmojiSize) && (
|
||||||
<ReactionAnimatedEmoji
|
<ReactionAnimatedEmoji
|
||||||
|
|||||||
98
src/components/story/mediaArea/MediaAreaWeather.tsx
Normal file
98
src/components/story/mediaArea/MediaAreaWeather.tsx
Normal 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));
|
||||||
@ -222,6 +222,7 @@ export const MENU_TRANSITION_DURATION = 200;
|
|||||||
export const SLIDE_TRANSITION_DURATION = 450;
|
export const SLIDE_TRANSITION_DURATION = 450;
|
||||||
|
|
||||||
export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji';
|
export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji';
|
||||||
|
export const RESTRICTED_EMOJI_SET = 'RestrictedEmoji';
|
||||||
|
|
||||||
export const VIDEO_WEBM_TYPE = 'video/webm';
|
export const VIDEO_WEBM_TYPE = 'video/webm';
|
||||||
export const GIF_MIME_TYPE = 'image/gif';
|
export const GIF_MIME_TYPE = 'image/gif';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type {
|
|||||||
import type { RequiredGlobalActions } from '../../index';
|
import type { RequiredGlobalActions } from '../../index';
|
||||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
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 { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { oldTranslate } from '../../../util/oldLangProvider';
|
import { oldTranslate } from '../../../util/oldLangProvider';
|
||||||
@ -286,6 +286,26 @@ addActionHandler('loadBirthdayNumbersStickers', async (global): Promise<void> =>
|
|||||||
setGlobal(global);
|
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> => {
|
addActionHandler('loadGenericEmojiEffects', async (global): Promise<void> => {
|
||||||
const stickerSet = await callApi('fetchGenericEmojiEffects');
|
const stickerSet = await callApi('fetchGenericEmojiEffects');
|
||||||
if (!stickerSet) {
|
if (!stickerSet) {
|
||||||
|
|||||||
@ -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);
|
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) {
|
export function selectAnimatedEmojiEffect<T extends GlobalState>(global: T, emoji: string) {
|
||||||
const { animatedEmojiEffects } = global;
|
const { animatedEmojiEffects } = global;
|
||||||
if (!animatedEmojiEffects || !animatedEmojiEffects.stickers) {
|
if (!animatedEmojiEffects || !animatedEmojiEffects.stickers) {
|
||||||
|
|||||||
@ -1150,6 +1150,7 @@ export type GlobalState = {
|
|||||||
animatedEmojiEffects?: ApiStickerSet;
|
animatedEmojiEffects?: ApiStickerSet;
|
||||||
genericEmojiEffects?: ApiStickerSet;
|
genericEmojiEffects?: ApiStickerSet;
|
||||||
birthdayNumbers?: ApiStickerSet;
|
birthdayNumbers?: ApiStickerSet;
|
||||||
|
restrictedEmoji?: ApiStickerSet;
|
||||||
defaultTopicIconsId?: string;
|
defaultTopicIconsId?: string;
|
||||||
defaultStatusIconsId?: string;
|
defaultStatusIconsId?: string;
|
||||||
premiumGifts?: ApiStickerSet;
|
premiumGifts?: ApiStickerSet;
|
||||||
@ -2892,6 +2893,7 @@ export interface ActionPayloads {
|
|||||||
loadGreetingStickers: undefined;
|
loadGreetingStickers: undefined;
|
||||||
loadGenericEmojiEffects: undefined;
|
loadGenericEmojiEffects: undefined;
|
||||||
loadBirthdayNumbersStickers: undefined;
|
loadBirthdayNumbersStickers: undefined;
|
||||||
|
loadRestrictedEmojiStickers: undefined;
|
||||||
|
|
||||||
loadAvailableEffects: undefined;
|
loadAvailableEffects: undefined;
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
|
|||||||
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
|
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
|
||||||
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
||||||
const LOW_PRIORITY_CACHE_MODULO = 0;
|
const LOW_PRIORITY_CACHE_MODULO = 0;
|
||||||
|
const CANVAS_CLASS = 'rlottie-canvas';
|
||||||
|
|
||||||
const workers = launchMediaWorkers().map(({ connector }) => connector);
|
const workers = launchMediaWorkers().map(({ connector }) => connector);
|
||||||
const instancesByRenderId = new Map<string, RLottie>();
|
const instancesByRenderId = new Map<string, RLottie>();
|
||||||
@ -281,6 +282,8 @@ class RLottie {
|
|||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d')!;
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
canvas.classList.add(CANVAS_CLASS);
|
||||||
|
|
||||||
canvas.style.width = `${size}px`;
|
canvas.style.width = `${size}px`;
|
||||||
canvas.style.height = `${size}px`;
|
canvas.style.height = `${size}px`;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
import { preloadImage } from './files';
|
import { preloadImage } from './files';
|
||||||
|
|
||||||
|
const LUMA_THRESHOLD = 128;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HEX > RGB
|
* HEX > RGB
|
||||||
* input: 'xxxxxx' (ex. 'ed15fa') case-insensitive
|
* 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)`;
|
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';
|
||||||
|
};
|
||||||
|
|||||||
4
src/util/formatTemperature.ts
Normal file
4
src/util/formatTemperature.ts
Normal 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`;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user