Skip to content

Commit c997c6a

Browse files
committed
Docs and emoji tagged template literal function
1 parent e2af836 commit c997c6a

File tree

9 files changed

+128
-10
lines changed

9 files changed

+128
-10
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ To be released.
2525

2626
- Added `Emoji` type.
2727
- Added `isEmoji()` predicate function.
28+
- Added `emoji()` tagged template literal function.
2829
- Added `Message.react()` method.
2930
- Added `Reaction` interface.
3031
- Added `AuthorizedReaction` interface.

docs/concepts/message.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,59 @@ await like.unlike(); // [!code highlight]
535535
~~~~
536536

537537

538+
Reacting to a message with an emoji
539+
-----------------------------------
540+
541+
*This API is available since BotKit 0.2.0.*
542+
543+
You can react to a message with an emoji by calling the `~Message.react()`
544+
method:
545+
546+
~~~~ typescript
547+
const message = await session.publish(
548+
text`This message will be reacted to with an emoji.`
549+
);
550+
await message.react(emoji`👍`); // [!code highlight]
551+
~~~~
552+
553+
> [!NOTE]
554+
> The tagged template literal function `emoji()` takes a string and returns
555+
> an `Emoji` value, which is a brand of `string`. If it takes anything other
556+
> than a single emoji, it will throw a `TypeError` at runtime.
557+
558+
Or you can use the `~Message.react()` method with a custom emoji. You need to
559+
define custom emojis in advance by calling the `Bot.addCustomEmojis()` method:
560+
561+
~~~~ typescript
562+
const emojis = bot.addCustomEmojis({
563+
// Use a remote image URL:
564+
yesBlob: {
565+
url: "https://cdn3.emoji.gg/emojis/68238-yesblob.png",
566+
mediaType: "image/png",
567+
},
568+
// Use a local image file:
569+
noBlob: {
570+
file: `${import.meta.dirname}/emojis/no_blob.png`,
571+
mediaType: "image/webp",
572+
},
573+
});
574+
~~~~
575+
576+
Then you can use the custom emojis in the `~Message.react()` method:
577+
578+
~~~~ typescript
579+
await message.react(emojis.yesBlob);
580+
~~~~
581+
582+
If you need to undo the reaction, you can call
583+
the `~AuthorizedReaction.unreact()` method:
584+
585+
~~~~ typescript
586+
const reaction = await message.react(emojis.noBlob);
587+
await reaction.unreact(); // [!code highlight]
588+
~~~~
589+
590+
538591
Replying to a message
539592
---------------------
540593

src/emoji.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
import { assert } from "@std/assert/assert";
1717
import { assertFalse } from "@std/assert/false";
18-
import { isEmoji } from "./emoji.ts";
18+
import { emoji, isEmoji } from "./emoji.ts";
1919

2020
Deno.test("isEmoji() with valid emojis", () => {
2121
const validEmojis = [
@@ -112,3 +112,47 @@ Deno.test("isEmoji() with tricky invalid inputs", () => {
112112
);
113113
}
114114
});
115+
Deno.test("emoji() tagged template function with valid emojis", () => {
116+
const validEmojis = [
117+
emoji`😀`, // simple emoji
118+
emoji`👍`, // thumbs up
119+
emoji`🚀`, // rocket
120+
emoji`🏳️‍🌈`, // pride flag
121+
emoji`👨‍👩‍👧‍👦`, // family
122+
emoji`👩🏽‍🔬`, // woman scientist with medium skin tone
123+
emoji`🧘🏻‍♀️`, // woman in lotus position
124+
emoji`🇯🇵`, // flag
125+
];
126+
127+
for (const emojiValue of validEmojis) {
128+
assert(isEmoji(emojiValue));
129+
}
130+
});
131+
132+
Deno.test("emoji() tagged template function with interpolation", () => {
133+
const rocket = "🚀";
134+
const result = emoji`${rocket}`;
135+
assert(isEmoji(result));
136+
assert(result === "🚀");
137+
});
138+
139+
Deno.test("emoji() throws with invalid inputs", () => {
140+
const invalidInputs = [
141+
() => emoji`😀😀`, // multiple emojis
142+
() => emoji`hi😀`, // mixed content
143+
() => emoji`👍awesome`, // mixed content
144+
() => emoji` 😀`, // emoji with leading space
145+
() => emoji`😀 `, // emoji with trailing space
146+
() => emoji``, // empty string
147+
];
148+
149+
for (const fn of invalidInputs) {
150+
try {
151+
fn();
152+
assert(false, "Expected function to throw TypeError");
153+
} catch (error) {
154+
assert(error instanceof TypeError);
155+
assert(error.message.startsWith("Invalid emoji:"));
156+
}
157+
}
158+
});

src/emoji.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ export function isEmoji(value: unknown): value is Emoji {
4646
return /\p{Emoji}/u.test(segments[0].segment);
4747
}
4848

49+
/**
50+
* A tagged template literal function that creates an {@link Emoji} from
51+
* a string. It is a simple wrapper around the `String.raw` function,
52+
* but it also checks if the resulting string is a valid emoji.
53+
* @param strings The template strings.
54+
* @param values The values to interpolate into the template strings.
55+
* @returns The resulting {@link Emoji} value.
56+
* @throws {TypeError} If the resulting string is not a valid emoji.
57+
* @since 0.2.0
58+
*/
59+
export function emoji(
60+
strings: TemplateStringsArray,
61+
...values: unknown[]
62+
): Emoji {
63+
const result = String.raw(strings, ...values);
64+
if (!isEmoji(result)) {
65+
throw new TypeError(`Invalid emoji: ${result}`);
66+
}
67+
return result;
68+
}
69+
4970
/**
5071
* The common interface for defining custom emojis.
5172
* @since 0.2.0

src/message-impl.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { assertEquals } from "@std/assert/equals";
3939
import { assertInstanceOf } from "@std/assert/instance-of";
4040
import { assertRejects } from "@std/assert/rejects";
4141
import { BotImpl } from "./bot-impl.ts";
42-
import { type DeferredCustomEmoji, isEmoji } from "./emoji.ts";
42+
import { type DeferredCustomEmoji, emoji } from "./emoji.ts";
4343
import {
4444
createMessage,
4545
getMessageClass,
@@ -609,9 +609,7 @@ Deno.test("MessageImpl.react()", async (t) => {
609609

610610
await t.step("react() with string emoji", async () => {
611611
ctx.sentActivities = []; // Clear previous activities
612-
const emoji = "👍";
613-
assert(isEmoji(emoji));
614-
const reaction = await message.react(emoji);
612+
const reaction = await message.react(emoji`👍`);
615613
assertEquals(ctx.sentActivities.length, 2);
616614
const { recipients, activity } = ctx.sentActivities[0];
617615
assertEquals(recipients, "followers");
@@ -629,7 +627,7 @@ Deno.test("MessageImpl.react()", async (t) => {
629627
assertEquals(reaction.raw, activity);
630628
assertEquals(reaction.id, activity.id);
631629
assertEquals(reaction.message, message);
632-
assertEquals(reaction.emoji, emoji);
630+
assertEquals(reaction.emoji, emoji`👍`);
633631

634632
// Test unreact
635633
ctx.sentActivities = [];

src/message-impl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
Create,
2323
Delete,
2424
Document,
25-
Emoji as CustomEmoji,
25+
type Emoji as CustomEmoji,
2626
EmojiReact,
2727
Hashtag,
2828
isActor,
@@ -41,7 +41,7 @@ import type { LanguageTag } from "@phensley/language-tag";
4141
import { unescape } from "@std/html/entities";
4242
import { generate as uuidv7 } from "@std/uuid/unstable-v7";
4343
import { FilterXSS, getDefaultWhiteList } from "xss";
44-
import { DeferredCustomEmoji, Emoji } from "./emoji.ts";
44+
import type { DeferredCustomEmoji, Emoji } from "./emoji.ts";
4545
import type {
4646
AuthorizedMessage,
4747
AuthorizedSharedMessage,

src/message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type {
2525
Question,
2626
} from "@fedify/fedify/vocab";
2727
import type { LanguageTag } from "@phensley/language-tag";
28-
import { DeferredCustomEmoji, Emoji } from "./emoji.ts";
28+
import type { DeferredCustomEmoji, Emoji } from "./emoji.ts";
2929
import type { AuthorizedLike, AuthorizedReaction } from "./reaction.ts";
3030
import type {
3131
SessionPublishOptions,

src/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {
3636
type CustomEmoji,
3737
type DeferredCustomEmoji,
3838
type Emoji,
39+
emoji,
3940
isEmoji,
4041
} from "./emoji.ts";
4142
export type * from "./events.ts";

src/reaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
} from "@fedify/fedify/vocab";
2222
import type { Emoji } from "./emoji.ts";
2323
import type { Message, MessageClass } from "./message.ts";
24-
export { type Actor, Like as RawLike } from "@fedify/fedify/vocab";
24+
export { type Actor, EmojiReact, Like as RawLike } from "@fedify/fedify/vocab";
2525

2626
/**
2727
* A like of a message. It is a thin wrapper around a `Like`, which is

0 commit comments

Comments
 (0)