mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(flags): add comment flags (#1140)
* feat(flags): add comment flags * fix(flags): address review feedback * fix(flags): clear stale flag publish data
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getBoardFlagDefinition, normalizeBoardFlagKind } from '../board-flags';
|
||||
|
||||
describe('board-flags', () => {
|
||||
it('normalizes board flag kind aliases', () => {
|
||||
expect(normalizeBoardFlagKind('pol')).toBe('pol');
|
||||
expect(normalizeBoardFlagKind('memeflags')).toBe('pol');
|
||||
expect(normalizeBoardFlagKind('mlp')).toBe('pony');
|
||||
expect(normalizeBoardFlagKind('unknown')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps political flags to the flags-2 sprite grid', () => {
|
||||
expect(getBoardFlagDefinition('pol', 'AC')).toMatchObject({
|
||||
code: 'AC',
|
||||
label: 'Anarcho-Capitalist',
|
||||
spritePath: 'assets/icons/flags-2.png',
|
||||
width: 16,
|
||||
height: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
expect(getBoardFlagDefinition('pol', 'WP')).toMatchObject({ code: 'WP', x: 64, y: 48 });
|
||||
});
|
||||
|
||||
it('maps pony flags to the flags-pony sprite grid', () => {
|
||||
expect(getBoardFlagDefinition('pony', '4CC')).toMatchObject({
|
||||
code: '4CC',
|
||||
label: '4cc /mlp/',
|
||||
spritePath: 'assets/icons/flags-pony.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
expect(getBoardFlagDefinition('pony', 'AJ')).toMatchObject({ code: 'AJ', label: 'Applejack', x: 80, y: 0 });
|
||||
expect(getBoardFlagDefinition('pony', 'ZS')).toMatchObject({ code: 'ZS', label: 'Zipp Storm', x: 16, y: 144 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsFromSelection, getCommentFlagRequestFromSelection } from '../comment-flag-selection';
|
||||
|
||||
describe('comment-flag-selection', () => {
|
||||
it('does not expose options for boards without flags', () => {
|
||||
expect(getCommentFlagOptionsForDirectory({ features: {}, title: '/mu/ - Music' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses geographic location as the default for country flag boards', () => {
|
||||
expect(
|
||||
getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'int',
|
||||
features: { hasFlags: true },
|
||||
title: '/int/ - International',
|
||||
}),
|
||||
).toEqual([{ label: 'Geographic Location', value: 'country:auto' }]);
|
||||
});
|
||||
|
||||
it('matches the 4chan /pol/ flag order', () => {
|
||||
const options = getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'pol',
|
||||
features: { hasFlags: true },
|
||||
title: '/pol/ - Politically Incorrect',
|
||||
});
|
||||
|
||||
expect(options.slice(0, 6)).toEqual([
|
||||
{ label: 'Geographic Location', value: 'country:auto' },
|
||||
{ label: 'Anarcho-Capitalist', value: 'pol:AC' },
|
||||
{ label: 'Anarchist', value: 'pol:AN' },
|
||||
{ label: 'Black Nationalist', value: 'pol:BL' },
|
||||
{ label: 'Confederate', value: 'pol:CF' },
|
||||
{ label: 'Communist', value: 'pol:CM' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('matches the 4chan /mlp/ flag default', () => {
|
||||
const options = getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'mlp',
|
||||
features: { hasFlags: true },
|
||||
title: '/mlp/ - Pony',
|
||||
});
|
||||
|
||||
expect(options.slice(0, 4)).toEqual([
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: '4cc /mlp/', value: 'pony:4CC' },
|
||||
{ label: 'Adagio Dazzle', value: 'pony:ADA' },
|
||||
{ label: 'Anon', value: 'pony:AN' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to the board title when directoryCode is blank', () => {
|
||||
const options = getCommentFlagOptionsForDirectory({
|
||||
directoryCode: ' ',
|
||||
features: { hasFlags: true },
|
||||
title: '/mlp/ - Pony',
|
||||
});
|
||||
|
||||
expect(options.slice(0, 2)).toEqual([
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: '4cc /mlp/', value: 'pony:4CC' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('serializes selected flags as challenge-readable flag requests', () => {
|
||||
expect(getCommentFlagRequestFromSelection('country:auto')).toEqual({
|
||||
type: 'country',
|
||||
code: 'auto',
|
||||
text: 'flag:country:auto',
|
||||
});
|
||||
expect(getCommentFlagRequestFromSelection('pol:AC')).toEqual({
|
||||
type: 'pol',
|
||||
code: 'AC',
|
||||
text: 'flag:pol:AC',
|
||||
});
|
||||
expect(getCommentFlagRequestFromSelection('pony:AJ')).toEqual({
|
||||
type: 'pony',
|
||||
code: 'AJ',
|
||||
text: 'flag:pony:AJ',
|
||||
});
|
||||
expect(getCommentFlagRequestFromSelection('none')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('publishes selected flags as signed comment flairs and challenge answers', () => {
|
||||
expect(getCommentFlagPublishOptionsFromSelection('country:auto')).toEqual({
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
|
||||
});
|
||||
expect(getCommentFlagPublishOptionsFromSelection('pony:AJ')).toEqual({
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pony:AJ'],
|
||||
},
|
||||
flairs: [{ type: 'pony', code: 'AJ', text: 'flag:pony:AJ' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('clears flag publish data when no flag is selected', () => {
|
||||
expect(getCommentFlagPublishOptionsFromSelection('none')).toEqual({
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
});
|
||||
expect(getCommentFlagPublishOptionsFromSelection(undefined)).toEqual({
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getCommentFlagFlairs, getAuthorFlagFlairs, getAuthorFlagViewModels } from '../comment-flags';
|
||||
|
||||
describe('comment-flags', () => {
|
||||
it('parses the pkc-js challenge flair example for country flags', () => {
|
||||
const [flag] = getAuthorFlagViewModels([{ text: 'US-emoji' }], 1000);
|
||||
|
||||
expect(flag).toMatchObject({
|
||||
key: 'country:us',
|
||||
type: 'country',
|
||||
code: 'us',
|
||||
spritePath: 'assets/icons/flags-1.png',
|
||||
width: 16,
|
||||
height: 11,
|
||||
x: 240,
|
||||
y: 154,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses namespaced country, political, and pony flag flairs', () => {
|
||||
const flags = getAuthorFlagViewModels([{ text: 'flag:country:DE' }, { text: 'flag:pol:AC' }, { text: 'flag:pony:AJ' }], 1000);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:de', 'pol:AC', 'pony:AJ']);
|
||||
expect(flags[1]).toMatchObject({ label: 'Anarcho-Capitalist', x: 0, y: 0 });
|
||||
expect(flags[2]).toMatchObject({ label: 'Applejack', x: 80, y: 0 });
|
||||
});
|
||||
|
||||
it('normalizes text flag kinds before parsing', () => {
|
||||
const flags = getAuthorFlagViewModels([{ text: 'FLAG:COUNTRY:DE' }, { text: 'POL:AC' }, { text: 'flag:MLP:AJ' }], 1000);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:de', 'pol:AC', 'pony:AJ']);
|
||||
});
|
||||
|
||||
it('parses structured flag flair fields', () => {
|
||||
const flags = getAuthorFlagViewModels(
|
||||
[
|
||||
{ text: 'ignored', type: 'Country', country: 'GB' },
|
||||
{ text: 'ignored', kind: 'MLP', code: 'RD' },
|
||||
],
|
||||
1000,
|
||||
);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:gb', 'pony:RD']);
|
||||
});
|
||||
|
||||
it('skips expired, duplicate, and unknown flags', () => {
|
||||
const flags = getAuthorFlagViewModels(
|
||||
[{ text: 'flag:country:DE', expiresAt: 999 }, { text: 'flag:country:DE' }, { text: 'DE-emoji' }, { text: 'flag:pony:NOPE' }],
|
||||
1000,
|
||||
);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:de']);
|
||||
});
|
||||
|
||||
it('prefers trusted challenge-written author community flairs', () => {
|
||||
const flairs = getAuthorFlagFlairs({
|
||||
flairs: [{ text: 'flag:pol:AC' }],
|
||||
community: {
|
||||
flairs: [{ text: 'US-emoji' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAuthorFlagViewModels(flairs).map((flag) => flag.key)).toEqual(['country:us']);
|
||||
});
|
||||
|
||||
it('reads the signed 5chan flag assertion from the comment', () => {
|
||||
const flairs = getCommentFlagFlairs({
|
||||
'5chan': {
|
||||
country: 'VN',
|
||||
flag: {
|
||||
code: 'VN',
|
||||
text: 'flag:country:vn',
|
||||
type: 'country',
|
||||
},
|
||||
},
|
||||
flairs: [{ code: 'auto', text: 'flag:country:auto', type: 'country' }],
|
||||
author: {
|
||||
community: {
|
||||
flairs: [{ text: 'US-emoji' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAuthorFlagViewModels(flairs).map((flag) => flag.key)).toEqual(['country:vn']);
|
||||
});
|
||||
|
||||
it('falls back to author or post flairs when there is no signed flag assertion', () => {
|
||||
expect(
|
||||
getAuthorFlagViewModels(
|
||||
getCommentFlagFlairs({
|
||||
author: {
|
||||
subplebbit: {
|
||||
flairs: [{ text: 'flag:country:DE' }],
|
||||
},
|
||||
},
|
||||
flairs: [{ text: 'flag:pony:AJ' }],
|
||||
}),
|
||||
).map((flag) => flag.key),
|
||||
).toEqual(['country:de']);
|
||||
|
||||
expect(
|
||||
getAuthorFlagViewModels(
|
||||
getCommentFlagFlairs({
|
||||
flairs: [{ text: 'flag:pony:AJ' }],
|
||||
}),
|
||||
).map((flag) => flag.key),
|
||||
).toEqual(['pony:AJ']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user