Path: blob/main/internal/messages/contentreactions.go
366 views
package messages12import (3"context"4"fmt"5"html"6"log/slog"7"strconv"8"strings"910"github.com/diamondburned/arikawa/v3/discord"11"github.com/diamondburned/gotk4-adwaita/pkg/adw"12"github.com/diamondburned/gotk4/pkg/core/gioutil"13"github.com/diamondburned/gotk4/pkg/glib/v2"14"github.com/diamondburned/gotk4/pkg/gtk/v4"15"github.com/diamondburned/gotkit/app"16"github.com/diamondburned/gotkit/app/locale"17"github.com/diamondburned/gotkit/components/onlineimage"18"github.com/diamondburned/gotkit/gtkutil"19"github.com/diamondburned/gotkit/gtkutil/cssutil"20"github.com/diamondburned/gotkit/gtkutil/imgutil"21"libdb.so/dissent/internal/gtkcord"22)2324type messageReaction struct {25discord.Reaction26GuildID discord.GuildID27ChannelID discord.ChannelID28MessageID discord.MessageID29}3031func (r messageReaction) Equal(other messageReaction) bool {32return true &&33r.MessageID == other.MessageID &&34r.ChannelID == other.ChannelID &&35r.Me == other.Me &&36r.Count == other.Count &&37r.Emoji.APIString() == other.Emoji.APIString()38}3940type contentReactions struct {41*gtk.FlowBox4243// *gtk.ScrolledWindow44// grid *gtk.GridView4546ctx context.Context47parent *Content48reactions *gioutil.ListModel[messageReaction]49}5051var reactionsCSS = cssutil.Applier("message-reactions", `52.message-reactions {53padding: 0;54margin-top: 4px;55background: none;56}57.message-reactions > flowboxchild {58margin: 4px 0;59margin-right: 6px;60padding: 0;61}62`)6364func newContentReactions(ctx context.Context, parent *Content) *contentReactions {65rs := contentReactions{66ctx: ctx,67parent: parent,68reactions: gioutil.NewListModel[messageReaction](),69}7071// TODO: complain to the GTK devs about how broken GridView is.72// Why is it not reflowing widgets? and other mysteries to solve in the GTK73// framework.7475// rs.grid = gtk.NewGridView(76// gtk.NewNoSelection(rs.reactions.ListModel),77// newContentReactionsFactory(ctx))78// rs.grid.SetOrientation(gtk.OrientationHorizontal)79// reactionsCSS(rs.grid)80//81// rs.ScrolledWindow = gtk.NewScrolledWindow()82// rs.ScrolledWindow.SetPolicy(gtk.PolicyNever, gtk.PolicyNever)83// rs.ScrolledWindow.SetPropagateNaturalWidth(true)84// rs.ScrolledWindow.SetPropagateNaturalHeight(false)85// rs.ScrolledWindow.SetChild(rs.grid)8687rs.FlowBox = gtk.NewFlowBox()88rs.FlowBox.SetOrientation(gtk.OrientationHorizontal)89rs.FlowBox.SetHomogeneous(true)90rs.FlowBox.SetMaxChildrenPerLine(30)91rs.FlowBox.SetSelectionMode(gtk.SelectionNone)92reactionsCSS(rs)9394rs.FlowBox.BindModel(rs.reactions.ListModel, func(o *glib.Object) gtk.Widgetter {95reaction := gioutil.ObjectValue[messageReaction](o)96w := newContentReaction()97w.SetReaction(ctx, rs.FlowBox, reaction)98return w99})100101gtkutil.BindActionCallbackMap(rs, map[string]gtkutil.ActionCallback{102"reactions.toggle": {103ArgType: glib.NewVariantType("s"),104Func: func(args *glib.Variant) {105emoji := discord.APIEmoji(args.String())106selected := rs.isReacted(emoji)107108client := gtkcord.FromContext(rs.ctx).Online()109gtkutil.Async(rs.ctx, func() func() {110var err error111if selected {112err = client.Unreact(rs.parent.ChannelID(), rs.parent.MessageID(), emoji)113} else {114err = client.React(rs.parent.ChannelID(), rs.parent.MessageID(), emoji)115}116117if err != nil {118if selected {119err = fmt.Errorf("failed to react: %w", err)120} else {121err = fmt.Errorf("failed to unreact: %w", err)122}123app.Error(rs.ctx, err)124}125126return nil127})128},129},130})131132return &rs133}134135func (rs *contentReactions) findReactionIx(emoji discord.APIEmoji) int {136var i int137foundIx := -1138139iter := rs.reactions.All()140iter(func(reaction messageReaction) bool {141if reaction.Emoji.APIString() == emoji {142foundIx = i143return false144}145i++146return true147})148149return foundIx150}151152func (rs *contentReactions) isReacted(emoji discord.APIEmoji) bool {153ix := rs.findReactionIx(emoji)154if ix == -1 {155return false156}157return rs.reactions.At(ix).Me158}159160// SetReactions sets the reactions of the message.161//162// TODO: implement Add and Remove event handlers directly in this container to163// avoid having to clear the whole list.164func (rs *contentReactions) SetReactions(reactions []discord.Reaction) {165messageReactions := make([]messageReaction, len(reactions))166for i, r := range reactions {167messageReactions[i] = messageReaction{168Reaction: r,169GuildID: rs.parent.view.GuildID(),170ChannelID: rs.parent.view.ChannelID(),171MessageID: rs.parent.MessageID(),172}173}174rs.reactions.Splice(0, rs.reactions.Len(), messageReactions...)175}176177/*178func newContentReactionsFactory(ctx context.Context) *gtk.ListItemFactory {179reactionWidgets := make(map[uintptr]*contentReaction)180181factory := gtk.NewSignalListItemFactory()182factory.ConnectSetup(func(item *gtk.ListItem) {183w := newContentReaction()184item.SetChild(w)185reactionWidgets[item.Native()] = w186})187factory.ConnectTeardown(func(item *gtk.ListItem) {188item.SetChild(nil)189delete(reactionWidgets, item.Native())190})191192factory.ConnectBind(func(item *gtk.ListItem) {193reaction := gioutil.ObjectValue[messageReaction](item.Item())194195w := reactionWidgets[item.Native()]196w.SetReaction(ctx, reaction)197})198factory.ConnectUnbind(func(item *gtk.ListItem) {199w := reactionWidgets[item.Native()]200w.Clear()201})202203return &factory.ListItemFactory204}205*/206207type reactionsLoadState uint8208209const (210reactionsNotLoaded reactionsLoadState = iota211reactionsLoading212reactionsLoaded213)214215type contentReaction struct {216*gtk.ToggleButton217iconBin *adw.Bin218countLabel *gtk.Label219220reaction messageReaction221client *gtkcord.State222223tooltip string224tooltipState reactionsLoadState225}226227var reactionCSS = cssutil.Applier("message-reaction", `228.message-reaction {229/* min-width: 4em; */230min-width: 0;231min-height: 0;232padding: 0;233}234.message-reaction > box {235margin: 6px;236}237.message-reaction-emoji-icon {238min-width: 22px;239min-height: 22px;240}241.message-reaction-emoji-unicode {242font-size: 18px;243}244`)245246func newContentReaction() *contentReaction {247r := contentReaction{}248249r.ToggleButton = gtk.NewToggleButton()250r.ToggleButton.AddCSSClass("message-reaction")251r.ToggleButton.ConnectClicked(func() {252r.SetSensitive(false)253254ok := r.ActivateAction("reactions.toggle", glib.NewVariantString(string(r.reaction.Emoji.APIString())))255if !ok {256slog.Error(257"failed to activate reactions.toggle",258"emoji", r.reaction.Emoji.APIString())259}260})261262r.ToggleButton.SetHasTooltip(true)263r.ToggleButton.ConnectQueryTooltip(func(_, _ int, _ bool, tooltip *gtk.Tooltip) bool {264tooltip.SetText(locale.Get("Loading..."))265r.invalidateUsers(tooltip.SetMarkup)266return true267})268269r.iconBin = adw.NewBin()270r.iconBin.AddCSSClass("message-reaction-icon")271272r.countLabel = gtk.NewLabel("")273r.countLabel.AddCSSClass("message-reaction-count")274r.countLabel.SetHExpand(true)275r.countLabel.SetXAlign(1)276277box := gtk.NewBox(gtk.OrientationHorizontal, 0)278box.Append(r.iconBin)279box.Append(r.countLabel)280281r.ToggleButton.SetChild(box)282reactionCSS(r)283284return &r285}286287// SetReaction sets the reaction of the widget.288func (r *contentReaction) SetReaction(ctx context.Context, flowBox *gtk.FlowBox, reaction messageReaction) {289r.reaction = reaction290r.client = gtkcord.FromContext(ctx).Online()291292if reaction.Emoji.IsCustom() {293emoji := onlineimage.NewPicture(ctx, imgutil.HTTPProvider)294emoji.AddCSSClass("message-reaction-emoji")295emoji.AddCSSClass("message-reaction-emoji-custom")296emoji.SetSizeRequest(gtkcord.InlineEmojiSize, gtkcord.InlineEmojiSize)297emoji.SetKeepAspectRatio(true)298emoji.SetURL(reaction.Emoji.EmojiURL())299300// TODO: get this working:301// Currently, it just jitters in size. The button itself can still be302// sized small, FlowBox is just forcing it to be big. This does mean303// that it's not the GIF that is causing this.304305// anim := emoji.EnableAnimation()306// anim.ConnectMotion(r)307308r.iconBin.SetChild(emoji)309} else {310label := gtk.NewLabel(reaction.Emoji.Name)311label.AddCSSClass("message-reaction-emoji")312label.AddCSSClass("message-reaction-emoji-unicode")313314r.iconBin.SetChild(label)315}316317r.countLabel.SetLabel(strconv.Itoa(reaction.Count))318319r.ToggleButton.SetActive(reaction.Me)320if reaction.Me {321r.AddCSSClass("message-reaction-me")322} else {323r.RemoveCSSClass("message-reaction-me")324}325}326327func (r *contentReaction) Clear() {328r.reaction = messageReaction{}329r.client = nil330r.tooltipState = reactionsNotLoaded331r.iconBin.SetChild(nil)332r.ToggleButton.SetActive(false)333r.ToggleButton.RemoveCSSClass("message-reaction-me")334}335336func (r *contentReaction) invalidateUsers(callback func(string)) {337if r.tooltipState != reactionsNotLoaded {338callback(r.tooltip)339return340}341342r.tooltipState = reactionsLoading343r.tooltip = ""344345reaction := r.reaction346client := r.client347348var tooltip string349if reaction.Emoji.IsCustom() {350tooltip = ":" + html.EscapeString(reaction.Emoji.Name) + ":\n"351}352353done := func(tooltip string, err error) {354glib.IdleAdd(func() {355if !r.reaction.Equal(reaction) {356// The reaction has changed,357// so we don't care about the result.358return359}360361if err != nil {362r.tooltipState = reactionsNotLoaded363r.tooltip = tooltip + "<b>" + locale.Get("Error: ") + "</b>" + err.Error()364365slog.Error(366"cannot load reaction tooltip",367"channel", reaction.ChannelID,368"message", reaction.MessageID,369"emoji", reaction.Emoji.APIString(),370"err", err)371} else {372r.tooltipState = reactionsLoaded373r.tooltip = tooltip374}375376callback(r.tooltip)377})378}379380go func() {381u, err := client.Reactions(382reaction.ChannelID,383reaction.MessageID,384reaction.Emoji.APIString(), 11)385if err != nil {386done(tooltip, err)387return388}389390var hasMore bool391if len(u) > 10 {392hasMore = true393u = u[:10]394}395396for _, user := range u {397tooltip += fmt.Sprintf(398`<span size="small">%s</span>`+"\n",399client.MemberMarkup(reaction.GuildID, &discord.GuildUser{User: user}),400)401}402403if hasMore {404tooltip += "..."405} else {406tooltip = strings.TrimRight(tooltip, "\n")407}408409done(tooltip, nil)410}()411}412413414