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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
height: var(--custom-emoji-size);
|
||||
position: relative;
|
||||
flex: 0 0 var(--custom-emoji-size);
|
||||
|
||||
:global(.rlottie-canvas) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
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 BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji';
|
||||
export const RESTRICTED_EMOJI_SET = 'RestrictedEmoji';
|
||||
|
||||
export const VIDEO_WEBM_TYPE = 'video/webm';
|
||||
export const GIF_MIME_TYPE = 'image/gif';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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`;
|
||||
|
||||
|
||||
@ -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';
|
||||
};
|
||||
|
||||
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