cinera.c: Distinguish speakers from chat comments

It treats co-hosts and guests differently from chat commenters, styling
and categorising annotations for them such that their contributions
don't come under the "Chat comment" medium

Also do some essentially cosmetic code compression of the marker cases
and other things

cinera_player_pre.js: Make the credits menu initially focus the host's
person if they have no support, rather than the first credited person
who has support
This commit is contained in:
Matt Mascarenhas 2018-04-04 23:39:38 +01:00
parent 14afdc044d
commit 1efd808783
2 changed files with 282 additions and 138 deletions

View File

@ -14,7 +14,7 @@ typedef struct
version CINERA_APP_VERSION = { version CINERA_APP_VERSION = {
.Major = 0, .Major = 0,
.Minor = 5, .Minor = 5,
.Patch = 41 .Patch = 42
}; };
// TODO(matt): Copy in the DB 3 stuff from cinera_working.c // TODO(matt): Copy in the DB 3 stuff from cinera_working.c
@ -716,6 +716,18 @@ StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings
return *A - *B; return *A - *B;
} }
int
StringsDifferCaseInsensitive(char *A, char *B) // NOTE(matt): Two null-terminated strings
{
while(*A && *B &&
((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) ==
((*B >= 'A' && *B <= 'Z') ? *B + ('a' - 'A') : *B))
{
++A, ++B;
}
return *A - *B;
}
bool bool
StringsDifferT(char *A, // NOTE(matt): Null-terminated string StringsDifferT(char *A, // NOTE(matt): Null-terminated string
char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer) char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer)
@ -1146,7 +1158,7 @@ CharToColour(char Char)
return Colour; return Colour;
} }
hsl_colour * void
StringToColourHash(hsl_colour *Colour, char *String) StringToColourHash(hsl_colour *Colour, char *String)
{ {
Colour->Hue = 0; Colour->Hue = 0;
@ -1162,7 +1174,6 @@ StringToColourHash(hsl_colour *Colour, char *String)
Colour->Hue = Colour->Hue % 360; Colour->Hue = Colour->Hue % 360;
Colour->Saturation = Colour->Saturation % 26 + 74; Colour->Saturation = Colour->Saturation % 26 + 74;
return(Colour);
} }
char * char *
@ -1242,6 +1253,20 @@ ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType)
} }
} }
typedef struct
{
char Abbreviation[32];
hsl_colour Colour;
credential_info *Credential;
bool Seen;
} speaker;
typedef struct
{
speaker Speaker[16];
int Count;
} speakers;
enum enum
{ {
CreditsError_NoHost, CreditsError_NoHost,
@ -1250,13 +1275,18 @@ enum
} credits_errors; } credits_errors;
int int
SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role) SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role, speakers *Speakers)
{ {
bool Found = FALSE; bool Found = FALSE;
for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex) for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex)
{ {
if(!StringsDiffer(Person, Credentials[CredentialIndex].Username)) if(!StringsDiffer(Person, Credentials[CredentialIndex].Username))
{ {
if(Speakers)
{
Speakers->Speaker[Speakers->Count].Credential = &Credentials[CredentialIndex];
++Speakers->Count;
}
Found = TRUE; Found = TRUE;
if(*HasCreditsMenu == FALSE) if(*HasCreditsMenu == FALSE)
{ {
@ -1310,16 +1340,146 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char
" </span>\n"); " </span>\n");
} }
} }
return Found ? 0 : CreditsError_NoCredentials; return Found ? RC_SUCCESS : CreditsError_NoCredentials;
}
void
ClearString(char *String)
{
while(*String)
{
*String++ = '\0';
}
}
void
InitialString(char *Dest, char *Src)
{
ClearString(Dest);
*Dest++ = *Src++;
while(*Src++)
{
if(*Src == ' ')
{
++Src;
if(*Src)
{
*Dest++ = *Src;
}
}
}
}
void
GetFirstSubstring(char *Dest, char *Src)
{
ClearString(Dest);
while(*Src && *Src != ' ')
{
*Dest++ = *Src++;
}
}
void
InitialAndGetFinalString(char *Dest, char *Src)
{
ClearString(Dest);
int SrcLength = StringLength(Src);
char *SrcPtr = Src + SrcLength - 1;
while(SrcPtr > Src && *SrcPtr != ' ')
{
--SrcPtr;
}
if(*SrcPtr == ' ' && SrcPtr - Src < SrcLength - 1)
{
++SrcPtr;
}
if(Src < SrcPtr)
{
*Dest++ = *Src++;
*Dest++ = '.';
*Dest++ = ' ';
while(Src < SrcPtr - 1)
{
if(*Src == ' ')
{
++Src;
if(*Src)
{
*Dest++ = *Src;
*Dest++ = '.';
*Dest++ = ' ';
}
}
++Src;
}
}
CopyString(Dest, SrcPtr);
}
bool
AbbreviationsClash(speakers *Speakers)
{
for(int i = 0; i < Speakers->Count; ++i)
{
for(int j = i + 1; j < Speakers->Count; ++j)
{
if(!StringsDiffer(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation))
{
return TRUE;
}
}
}
return FALSE;
}
void
SortAndAbbreviateSpeakers(speakers *Speakers)
{
for(int i = 0; i < Speakers->Count; ++i)
{
for(int j = i + 1; j < Speakers->Count; ++j)
{
if(StringsDiffer(Speakers->Speaker[i].Credential->Username, Speakers->Speaker[j].Credential->Username) > 0)
{
credential_info *Temp = Speakers->Speaker[j].Credential;
Speakers->Speaker[j].Credential = Speakers->Speaker[i].Credential;
Speakers->Speaker[i].Credential = Temp;
break;
}
}
}
for(int i = 0; i < Speakers->Count; ++i)
{
StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Credential->Username);
InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName);
}
int Attempt = 0;
while(AbbreviationsClash(Speakers))
{
for(int i = 0; i < Speakers->Count; ++i)
{
switch(Attempt)
{
case 0: GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break;
case 1: InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break;
case 2: ClearCopyStringNoFormat(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->Username); break;
}
}
++Attempt;
}
} }
int int
BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata) BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers)
// TODO(matt): Make this take the Credentials, once we are parsing them from a config // TODO(matt): Make this take the Credentials, once we are parsing them from a config
{ {
if(Metadata->member) if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host", Speakers) == CreditsError_NoCredentials)
{
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host"))
{ {
printf("No credentials for member %s. Please contact matt@handmadedev.org with their:\n" printf("No credentials for member %s. Please contact matt@handmadedev.org with their:\n"
" Full name\n" " Full name\n"
@ -1327,24 +1487,12 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta
" Financial support info, e.g. Patreon URL (optional)\n", Metadata->member); " Financial support info, e.g. Patreon URL (optional)\n", Metadata->member);
return CreditsError_NoCredentials; return CreditsError_NoCredentials;
} }
}
else
{
if(*HasCreditsMenu == TRUE)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
fprintf(stderr, "Missing \"member\" in the [video] node\n");
return CreditsError_NoHost;
}
if(Metadata->co_host_count > 0) if(Metadata->co_host_count > 0)
{ {
for(int i = 0; i < Metadata->co_host_count; ++i) for(int i = 0; i < Metadata->co_host_count; ++i)
{ {
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host")) if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host", Speakers) == CreditsError_NoCredentials)
{ {
printf("No credentials for co-host %s. Please contact matt@handmadedev.org with their:\n" printf("No credentials for co-host %s. Please contact matt@handmadedev.org with their:\n"
" Full name\n" " Full name\n"
@ -1359,7 +1507,7 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta
{ {
for(int i = 0; i < Metadata->guest_count; ++i) for(int i = 0; i < Metadata->guest_count; ++i)
{ {
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest")) if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest", Speakers) == CreditsError_NoCredentials)
{ {
printf("No credentials for guest %s. Please contact matt@handmadedev.org with their:\n" printf("No credentials for guest %s. Please contact matt@handmadedev.org with their:\n"
" Full name\n" " Full name\n"
@ -1370,11 +1518,16 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta
} }
} }
if(Speakers->Count > 1)
{
SortAndAbbreviateSpeakers(Speakers);
}
if(Metadata->annotator_count > 0) if(Metadata->annotator_count > 0)
{ {
for(int i = 0; i < Metadata->annotator_count; ++i) for(int i = 0; i < Metadata->annotator_count; ++i)
{ {
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator")) if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator", 0) == CreditsError_NoCredentials)
{ {
printf("No credentials for annotator %s. Please contact matt@handmadedev.org with their:\n" printf("No credentials for annotator %s. Please contact matt@handmadedev.org with their:\n"
" Full name\n" " Full name\n"
@ -2713,6 +2866,32 @@ typedef struct
short int PrevIndex, ThisIndex, NextIndex; short int PrevIndex, ThisIndex, NextIndex;
} neighbourhood; } neighbourhood;
int
LinearSearchForSpeaker(speakers Speakers, char *Username)
{
for(int i = 0; i < Speakers.Count; ++i)
{
if(!StringsDifferCaseInsensitive(Speakers.Speaker[i].Credential->Username, Username))
{
return i;
}
}
return -1;
}
bool
IsCategorisedAFK(HMML_Annotation Anno)
{
for(int i = 0; i < Anno.marker_count; ++i)
{
if(!StringsDiffer(Anno.markers[i].marker, "afk"))
{
return TRUE;
}
}
return FALSE;
}
int int
HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filename, neighbourhood *N) HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filename, neighbourhood *N)
{ {
@ -3019,7 +3198,8 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen
CopyStringToBuffer(&CollationBuffers->Player, CopyStringToBuffer(&CollationBuffers->Player,
" <div class=\"markers\">\n"); " <div class=\"markers\">\n");
switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata)) speakers Speakers = { 0 };
switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata, &Speakers))
{ {
case CreditsError_NoHost: case CreditsError_NoHost:
case CreditsError_NoAnnotator: case CreditsError_NoAnnotator:
@ -3114,7 +3294,10 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen
CopyStringToBuffer(&AnnotationClass, CopyStringToBuffer(&AnnotationClass,
" class=\"marker"); " class=\"marker");
if(Anno->author) if((Anno->author || Speakers.Count > 1) && !IsCategorisedAFK(*Anno))
{
int SpeakerIndex;
if(Anno->author && (SpeakerIndex = LinearSearchForSpeaker(Speakers, Anno->author)) == -1)
{ {
if(!HasFilterMenu) if(!HasFilterMenu)
{ {
@ -3123,26 +3306,33 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored"); InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored");
hsl_colour AuthorColour; hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Anno->author); StringToColourHash(&AuthorColour, Anno->author);
if(Config.Edition == EDITION_NETWORK) // TODO(matt): That EDITION_NETWORK site database API-polling stuff
{
fprintf(stderr, "%s:%d - TODO(matt): Implement author hoverbox\n", __FILE__, __LINE__);
// NOTE(matt): We should get instructions on how to get this info in the config
CopyStringToBuffer(&Text,
"<a class=\"author\" href=\"https://handmade.network/m/%s\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</a> ",
Anno->author,
AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness,
AuthorColour.Hue, AuthorColour.Saturation,
Anno->author);
}
else
{
CopyStringToBuffer(&Text, CopyStringToBuffer(&Text,
"<span class=\"author\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ", "<span class=\"author\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ",
AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness, AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness,
AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Hue, AuthorColour.Saturation,
Anno->author); Anno->author);
} }
else
{
if(!Anno->author)
{
SpeakerIndex = LinearSearchForSpeaker(Speakers, HMML.metadata.member);
}
CopyStringToBuffer(&Text,
"<span class=\"author\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>: ",
Speakers.Speaker[SpeakerIndex].Colour.Hue,
Speakers.Speaker[SpeakerIndex].Colour.Saturation,
Speakers.Speaker[SpeakerIndex].Colour.Lightness,
Speakers.Speaker[SpeakerIndex].Colour.Hue,
Speakers.Speaker[SpeakerIndex].Colour.Saturation,
Speakers.Speaker[SpeakerIndex].Seen == FALSE ? Speakers.Speaker[SpeakerIndex].Credential->CreditedName : Speakers.Speaker[SpeakerIndex].Abbreviation);
Speakers.Speaker[SpeakerIndex].Seen = TRUE;
}
} }
char *InPtr = Anno->text; char *InPtr = Anno->text;
@ -3153,67 +3343,26 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen
if(MarkerIndex < Anno->marker_count && if(MarkerIndex < Anno->marker_count &&
InPtr - Anno->text == Anno->markers[MarkerIndex].offset) InPtr - Anno->text == Anno->markers[MarkerIndex].offset)
{ {
// TODO(matt): Consider switching on the Anno->markers[MarkerIndex].type and 100% ensuring this is all correct
// I wonder if HMML_CATEGORY should do InPtr += StringLength(Readable); like the others, and also whether HMML_MEMBER and HMML_PROJECT could be
// identical, except only for their class ("member" and "project" respectively)
// Pretty goddamn sure we can totally compress these cases, but let's do it tomorrow when we're fresh
char *Readable = Anno->markers[MarkerIndex].parameter char *Readable = Anno->markers[MarkerIndex].parameter
? Anno->markers[MarkerIndex].parameter ? Anno->markers[MarkerIndex].parameter
: Anno->markers[MarkerIndex].marker; : Anno->markers[MarkerIndex].marker;
if(Anno->markers[MarkerIndex].type == HMML_MEMBER) switch(Anno->markers[MarkerIndex].type)
{ {
hsl_colour MemberColour; case HMML_MEMBER:
StringToColourHash(&MemberColour, Anno->markers[MarkerIndex].marker); case HMML_PROJECT:
if(Config.Edition == EDITION_NETWORK)
{ {
fprintf(stderr, "%s:%d - TODO(matt): Implement member hoverbox\n", __FILE__, __LINE__); // TODO(matt): That EDITION_NETWORK site database API-polling stuff
// NOTE(matt): We should get instructions on how to get this info in the config hsl_colour Colour;
StringToColourHash(&Colour, Anno->markers[MarkerIndex].marker);
CopyStringToBuffer(&Text, CopyStringToBuffer(&Text,
"<a class=\"member\" href=\"https://handmade.network/m/%s\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</a>", "<span class=\"%s\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>",
Anno->markers[MarkerIndex].marker, Anno->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project",
MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness,
MemberColour.Hue, MemberColour.Saturation, Colour.Hue, Colour.Saturation,
StringLength(Readable), InPtr); Readable);
}
else
{
CopyStringToBuffer(&Text,
"<span class=\"member\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness,
MemberColour.Hue, MemberColour.Saturation,
StringLength(Readable), InPtr);
}
InPtr += StringLength(Readable); } break;
++MarkerIndex; case HMML_CATEGORY:
}
else if(Anno->markers[MarkerIndex].type == HMML_PROJECT)
{
hsl_colour ProjectColour;
StringToColourHash(&ProjectColour, Anno->markers[MarkerIndex].marker);
if(Config.Edition == EDITION_NETWORK)
{
fprintf(stderr, "%s:%d - TODO(matt): Implement project hoverbox\n", __FILE__, __LINE__);
// NOTE(matt): We should get instructions on how to get this info in the config
CopyStringToBuffer(&Text,
"<a class=\"project\" href=\"https://%s.handmade.network/\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</a>",
Anno->markers[MarkerIndex].marker,
ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness,
ProjectColour.Hue, ProjectColour.Saturation,
Readable);
}
else
{
CopyStringToBuffer(&Text,
"<span class=\"project\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>",
ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness,
ProjectColour.Hue, ProjectColour.Saturation,
Readable);
}
InPtr += StringLength(Readable);
++MarkerIndex;
}
else if(Anno->markers[MarkerIndex].type == HMML_CATEGORY)
{ {
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker)) switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
{ {
@ -3230,8 +3379,12 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen
HasFilterMenu = TRUE; HasFilterMenu = TRUE;
} }
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker); InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
++MarkerIndex; CopyStringToBuffer(&Text, Readable);
} break;
case HMML_MARKER_COUNT: break;
} }
InPtr += StringLength(Readable);
++MarkerIndex;
} }
while(RefIndex < Anno->reference_count && while(RefIndex < Anno->reference_count &&
@ -3356,14 +3509,9 @@ AppendedIdentifier:
} }
} }
if(Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset) CopyStringToBuffer(&Text, "<sup style=\"vertical-align: super;\">%s%d</sup>",
{ Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset ? "," : "",
CopyStringToBuffer(&Text, "<sup style=\"vertical-align: super;\">,%d</sup>", RefIdentifier); RefIdentifier);
}
else
{
CopyStringToBuffer(&Text, "<sup style=\"vertical-align: super;\">%d</sup>", RefIdentifier);
}
++RefIndex; ++RefIndex;
++RefIdentifier; ++RefIdentifier;
@ -3375,29 +3523,25 @@ AppendedIdentifier:
{ {
case '<': case '<':
CopyStringToBuffer(&Text, "&lt;"); CopyStringToBuffer(&Text, "&lt;");
InPtr++;
break; break;
case '>': case '>':
CopyStringToBuffer(&Text, "&gt;"); CopyStringToBuffer(&Text, "&gt;");
InPtr++;
break; break;
case '&': case '&':
CopyStringToBuffer(&Text, "&amp;"); CopyStringToBuffer(&Text, "&amp;");
InPtr++;
break; break;
case '\"': case '\"':
CopyStringToBuffer(&Text, "&quot;"); CopyStringToBuffer(&Text, "&quot;");
InPtr++;
break; break;
case '\'': case '\'':
CopyStringToBuffer(&Text, "&#39;"); CopyStringToBuffer(&Text, "&#39;");
InPtr++;
break; break;
default: default:
*Text.Ptr++ = *InPtr++; *Text.Ptr++ = *InPtr;
*Text.Ptr = '\0'; *Text.Ptr = '\0';
break; break;
} }
++InPtr;
} }
} }

View File

@ -388,7 +388,7 @@ function toggleMenuVisibility(element) {
{ {
if(!lastFocusedCreditItem) if(!lastFocusedCreditItem)
{ {
if(element.querySelectorAll(".credit .support")[0]) if(element.querySelectorAll(".credit .person")[0].nextElementSibling)
{ {
lastFocusedCreditItem = element.querySelectorAll(".credit .support")[0]; lastFocusedCreditItem = element.querySelectorAll(".credit .support")[0];
focusedElement = lastFocusedCreditItem; focusedElement = lastFocusedCreditItem;