Skip to content

Commit

Permalink
Merge pull request #580 from Telegram-Mini-Apps/feature/v8-emoji-status
Browse files Browse the repository at this point in the history
Emoji status-related functions
  • Loading branch information
heyqbnk authored Dec 3, 2024
2 parents a58fe0a + 8bd6d16 commit 2e5dfd0
Show file tree
Hide file tree
Showing 19 changed files with 361 additions and 13 deletions.
3 changes: 3 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": [
"docs",
"custom-playground",
"vue-template",
"svelte-template",
"nextjs-template",
"reactjs-template",
"solidjs-template"
Expand Down
5 changes: 5 additions & 0 deletions .changeset/hip-birds-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/sdk": minor
---

Add emoji status-related functionality.
5 changes: 5 additions & 0 deletions .changeset/hungry-mails-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/bridge": minor
---

Add methods and events connected with custom emoji set.
1 change: 1 addition & 0 deletions apps/docs/.vitepress/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const packagesLinksGenerator = (prefix: string = '') => {
]),
],
'Utilities': [{ url: 'utils', page: false }, fromEntries([
scope('emoji-status'),
scope('links'),
scope('privacy'),
scope('uncategorized'),
Expand Down
56 changes: 56 additions & 0 deletions apps/docs/packages/telegram-apps-sdk/2-x/utils/emoji-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Emoji Status

## `requestEmojiStatusAccess`

To request access to user emoji status update, use the `requestEmojiStatusAccess` function:

::: code-group

```ts [Using isAvailable]
import { requestEmojiStatusAccess } from '@telegram-apps/sdk';

if (requestEmojiStatusAccess.isAvailable()) {
const status = await requestEmojiStatusAccess();
}
```

```ts [Using ifAvailable]
import { requestEmojiStatusAccess } from '@telegram-apps/sdk';

const status = await requestEmojiStatusAccess.ifAvailable();
```

:::

## `setEmojiStatus`

To set an emoji status on user's behalf, use the `setEmojiStatus` function.

As the first argument, it accepts a custom emoji id. Optionally, you can pass the second
argument determining for how many seconds the status must be set.

::: code-group

```ts [Using isAvailable]
import { setEmojiStatus } from '@telegram-apps/sdk';

if (setEmojiStatus.isAvailable()) {
// Set for unlimited period of time.
await setEmojiStatus('5361800828313167608');

// Set for 1 day.
await setEmojiStatus('5361800828313167608', 86400);
}
```

```ts [Using ifAvailable]
import { setEmojiStatus } from '@telegram-apps/sdk';

// Set for unlimited period of time.
await setEmojiStatus.ifAvailable('5361800828313167608');

// Set for 1 day.
await setEmojiStatus.ifAvailable('5361800828313167608', 86400);
```

:::
26 changes: 26 additions & 0 deletions apps/docs/platform/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,32 @@ Custom method invocation completed.
| result | `unknown` | _Optional_. Method invocation result. |
| error | `string` | _Optional_. Method invocation error code. |

### `emoji_status_access_requested`

Available since: **v8.0**

Access to set custom emoji status was requested.

| Field | Type | Description |
|--------|----------|------------------------------------------------------------|
| status | `string` | Request status. Possible values: `allowed` or `cancelled`. |

### `emoji_status_failed`

Available since: **v8.0**

Failed to set custom emoji status.

| Field | Type | Description |
|-------|----------|-----------------------------------------------------------------------------------------|
| error | `string` | Emoji set failure reason. Possible values: `SUGGESTED_EMOJI_INVALID` or `USER_DECLINED` |

### `emoji_status_set`

Available since: **v8.0**

Custom emoji status set.

### `fullscreen_changed`

Available since: **v8.0**
Expand Down
19 changes: 18 additions & 1 deletion apps/docs/platform/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Opens the biometric access settings for bots. Useful when you need to request bi
access to users who haven't granted it yet.

> [!INFO]
> This method can be called only in response to user interaction with the Mini App interface
> This method can be called only in response to user interaction with the Mini App interface
> (e.g. a click inside the Mini App or on the main button)
### `web_app_biometry_request_access`
Expand Down Expand Up @@ -388,6 +388,12 @@ Requests the current content safe area information from Telegram.
As a result, Telegram triggers the
[**`content_safe_area_changed`**](events.md#content-safe-area-changed) event.

### `web_app_request_emoji_status_access`

Available since: **v8.0**

Shows a native popup requesting permission for the bot to manage user's emoji status.

### `web_app_request_fullscreen`

Available since: **v8.0**
Expand Down Expand Up @@ -445,6 +451,17 @@ Updates the Mini App bottom bar background color.
|-------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| color | `string` | The Mini App bottom bar background color in `#RRGGBB` format, or one of the values: `bg_color`, `secondary_bg_color` or `bottom_bar_bg_color` |

### `web_app_set_emoji_status`

Available since: **v8.0**

Opens a dialog allowing the user to set the specified custom emoji as their status.

| Field | Type | Description |
|-----------------|----------|----------------------------------------------------|
| custom_emoji_id | `string` | Custom emoji identifier to set. |
| duration | `number` | _Optional_. The status expiration time in seconds. |

### `web_app_set_header_color`

Available since: **v6.1**
Expand Down
29 changes: 28 additions & 1 deletion packages/bridge/src/events/types/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RGB } from '@telegram-apps/types';

import type {
import {
PhoneRequestedStatus,
InvoiceStatus,
WriteAccessRequestedStatus,
Expand All @@ -9,6 +9,8 @@ import type {
BiometryTokenUpdateStatus,
SafeAreaInsets,
FullScreenErrorStatus,
EmojiStatusAccessRequestedStatus,
EmojiStatusFailedError,
} from './misc.js';

/**
Expand Down Expand Up @@ -150,6 +152,31 @@ export interface Events {
*/
error?: string;
};
/**
* Request to set custom emoji status was requested.
* @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-access-requested
* @since v8.0
*/
emoji_status_access_requested: {
/**
* Request status.
*/
status: EmojiStatusAccessRequestedStatus;
};
/**
* Failed to set custom emoji status.
* @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-failed
* @since v8.0
*/
emoji_status_failed: {
error: EmojiStatusFailedError;
};
/**
* Custom emoji status set.
* @see https://docs.telegram-mini-apps.com/platform/events#emoji-status-set
* @since v8.0
*/
emoji_status_set: never;
/**
* App entered or exited fullscreen mode.
* @since v8.0
Expand Down
4 changes: 4 additions & 0 deletions packages/bridge/src/events/types/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export type InvoiceStatus =

export type PhoneRequestedStatus = 'sent' | 'cancelled' | string;

export type EmojiStatusAccessRequestedStatus = 'allowed' | string;

export type EmojiStatusFailedError = 'SUGGESTED_EMOJI_INVALID' | 'USER_DECLINED' | string;

export type WriteAccessRequestedStatus = 'allowed' | string;

export type BiometryType = 'finger' | 'face' | string;
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge/src/methods/supports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ describe.each<[
]],
['8.0', [
'web_app_request_fullscreen',
'web_app_exit_fullscreen'
'web_app_exit_fullscreen',
'web_app_set_emoji_status',
'web_app_request_emoji_status_access',
]],
])('%s', (version, methods) => {
const higher = increaseVersion(version, 1);
Expand Down
2 changes: 2 additions & 0 deletions packages/bridge/src/methods/supports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export function supports(
case 'web_app_request_content_safe_area':
case 'web_app_request_fullscreen':
case 'web_app_exit_fullscreen':
case 'web_app_set_emoji_status':
case 'web_app_request_emoji_status_access':
return versionLessOrEqual('8.0', paramOrVersion);
default:
return [
Expand Down
21 changes: 21 additions & 0 deletions packages/bridge/src/methods/types/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ export interface Methods {
* @see https://docs.telegram-mini-apps.com/platform/methods#web-app-request-content-safe-area
*/
web_app_request_content_safe_area: CreateMethodParams;
/**
* Shows a native popup requesting permission for the bot to manage user's emoji status.
* @since v8.0
* @see https://docs.telegram-mini-apps.com/platform/methods#web-app-request-emoji-status-access
*/
web_app_request_emoji_status_access: CreateMethodParams;
/**
* Requests to open the mini app in fullscreen.
* @since v8.0
Expand Down Expand Up @@ -344,6 +350,21 @@ export interface Methods {
*/
color: BottomBarColor;
}>;
/**
* Opens a dialog allowing the user to set the specified custom emoji as their status.
* @since v8.0
* @see https://docs.telegram-mini-apps.com/platform/methods#web-app-set-emoji-status
*/
web_app_set_emoji_status: CreateMethodParams<{
/**
* Custom emoji identifier to set.
*/
custom_emoji_id: string;
/**
* The status expiration time in seconds.
*/
duration?: number;
}>;
/**
* Updates the Mini App header color.
* @since v6.1
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export const ERR_NOT_INITIALIZED = 'ERR_NOT_INITIALIZED';
export const ERR_NOT_SUPPORTED = 'ERR_NOT_SUPPORTED';
export const ERR_NOT_MOUNTED = 'ERR_NOT_MOUNTED';
export const ERR_FULLSCREEN_FAILED = 'ERR_FULLSCREEN_FAILED';
export const ERR_EMOJI_STATUS_SET_FAILED = 'ERR_EMOJI_STATUS_SET_FAILED';
2 changes: 2 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from '@/scopes/components/settings-button/exports.js';
export * from '@/scopes/components/swipe-behavior/exports.js';
export * from '@/scopes/components/theme-params/exports.js';
export * from '@/scopes/components/viewport/exports.js';
export * from '@/scopes/utilities/emoji-status/exports.js';
export * from '@/scopes/utilities/links/exports.js';
export * from '@/scopes/utilities/privacy/exports.js';
export * from '@/scopes/utilities/uncategorized/exports.js';
Expand All @@ -41,6 +42,7 @@ export {
ERR_ALREADY_MOUNTING,
ERR_ALREADY_REQUESTING,
ERR_FULLSCREEN_FAILED,
ERR_EMOJI_STATUS_SET_FAILED,
} from '@/errors.js';
export { init, type InitOptions } from '@/init.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/scopes/createMountFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function createMountFn<R>(
mount: (options?: AsyncOptions) => R | CancelablePromise<R>,
onMounted: (result: R) => void,
isMounted: Signal<boolean>,
promise: Signal<CancelablePromise<R> | undefined>,
promise: Signal<CancelablePromise<Awaited<R>> | undefined>,
error: Signal<Error | undefined>,
): (options?: AsyncOptions) => CancelablePromise<void> {
const noConcurrent = signalifyAsyncFn(
Expand Down
63 changes: 54 additions & 9 deletions packages/sdk/src/scopes/signalifyAsyncFn.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,69 @@
import { batch, type Signal } from '@telegram-apps/signals';
import { type AsyncOptions, CancelablePromise, type TypedError } from '@telegram-apps/bridge';
import { CancelablePromise, If, type TypedError } from '@telegram-apps/bridge';
import { AnyFn } from '@/types.js';

type AllowedFn<R> = (options?: AsyncOptions) => R | CancelablePromise<R>;
type Signalified<Fn extends AnyFn, Cancelable extends boolean> = (...args: Parameters<Fn>) => If<
Cancelable,
CancelablePromise<ReturnType<Fn>>,
PromiseLike<ReturnType<Fn>>
>;

/**
* Function doing the following:
* 1. Prevents the wrapped function from being called concurrently.
* 2. Being called, updates the passed promise and error signals.
*
* As a result, the function returns a new one, returning a cancelable promise.
* @param fn - function to wrap.
* @param createPendingError - function that creates error in case of concurrent call
* @param promise - signal containing the execution promise
* @param error - signal containing the last call error.
* @param cancelable - is result cancelable. True by default.
*/
export function signalifyAsyncFn<Fn extends AnyFn>(
fn: Fn,
createPendingError: () => TypedError<any>,
promise: Signal<CancelablePromise<Awaited<ReturnType<Fn>>> | undefined>,
error: Signal<Error | undefined>,
cancelable?: true,
): Signalified<Fn, true>;

/**
* Function doing the following:
* 1. Prevents the wrapped function from being called concurrently.
* 2. Being called, updates the passed promise and error signals.
*
* As a result, the function returns a new one, returning a non-cancelable promise.
* @param fn - function to wrap.
* @param createPendingError - function that creates error in case of concurrent call
* @param promise - signal containing the execution promise
* @param error - signal containing the last call error.
* @param cancelable - is result cancelable. True by default.
*/
export function signalifyAsyncFn<Fn extends AnyFn>(
fn: Fn,
createPendingError: () => TypedError<any>,
promise: Signal<Promise<Awaited<ReturnType<Fn>>> | undefined>,
error: Signal<Error | undefined>,
cancelable: false,
): Signalified<Fn, false>;

// #__NO_SIDE_EFFECTS__
export function signalifyAsyncFn<Fn extends AllowedFn<Result>, Result>(
export function signalifyAsyncFn<Fn extends AnyFn>(
fn: Fn,
createPendingError: () => TypedError<any>,
promise: Signal<CancelablePromise<Result> | undefined>,
promise:
| Signal<CancelablePromise<Awaited<ReturnType<Fn>>> | undefined>
| Signal<Promise<Awaited<ReturnType<Fn>>> | undefined>,
error: Signal<Error | undefined>,
): Fn {
return Object.assign((options?: AsyncOptions): CancelablePromise<Result> => {
return CancelablePromise
cancelable?: boolean,
): Signalified<Fn, boolean> {
const PromiseConstructor = cancelable === undefined || cancelable
? CancelablePromise
: Promise;

return Object.assign((...args: Parameters<Fn>): PromiseLike<ReturnType<Fn>> => {
return PromiseConstructor
.resolve()
.then(async () => {
// Check if the operation is currently not in progress.
Expand All @@ -32,10 +75,12 @@ export function signalifyAsyncFn<Fn extends AllowedFn<Result>, Result>(

// Start performing the wrapped function.
batch(() => {
promise.set(CancelablePromise.resolve(fn(options)));
promise.set(
(PromiseConstructor as typeof Promise).resolve(fn(...args)) as unknown as any,
);
error.set(undefined);
});
let result: [completed: true, result: Result] | [completed: false, err: Error];
let result: [completed: true, result: ReturnType<Fn>] | [completed: false, err: Error];
try {
result = [true, await promise()!];
} catch (e) {
Expand Down
Loading

0 comments on commit 2e5dfd0

Please sign in to comment.