Coffee Time

Check-in [b43f96ceab]
Login

Check-in [b43f96ceab]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Convert insights to server-side rendering
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | main | trunk
Files: files | file ages | folders
SHA3-256: b43f96ceab09292ff2e294338acbaaa2651f9b2561157774d2a584ac2df997cc
User & Date: scstarkey 2024-07-09 19:53:31
Context
2024-07-09
20:13
Fixed log message check-in: 96a94a9c64 user: scstarkey tags: main, trunk
19:53
Convert insights to server-side rendering check-in: b43f96ceab user: scstarkey tags: main, trunk
2024-07-07
15:09
Make blog posts more readable check-in: 6d097f5856 user: scstarkey tags: main, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to src/main/java/app/coffeetime/models/events/Event.java.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
package app.coffeetime.models.events;

import app.coffeetime.models.IceboxTopic;
import app.coffeetime.models.Linkable;
import app.coffeetime.models.ParticipantInfo;
import app.coffeetime.models.RoundStatus;
import app.coffeetime.models.chat.ChatMessage;

import app.coffeetime.util.DateInfo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.time.DurationFormatUtils;







>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package app.coffeetime.models.events;

import app.coffeetime.models.IceboxTopic;
import app.coffeetime.models.Linkable;
import app.coffeetime.models.ParticipantInfo;
import app.coffeetime.models.RoundStatus;
import app.coffeetime.models.chat.ChatMessage;
import app.coffeetime.util.Crypto;
import app.coffeetime.util.DateInfo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.time.DurationFormatUtils;
943
944
945
946
947
948
949







950
951
952
953
954
955
956
        return trialBalloonStatus;
    }

    @Override
    public PVector<Link> getLinks() {
        return links;
    }








    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.JSON_STYLE)
                .append("now", now)
                .append("internalId", internalId)
                .append("id", id)







>
>
>
>
>
>
>







944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
        return trialBalloonStatus;
    }

    @Override
    public PVector<Link> getLinks() {
        return links;
    }

    public String getInsightHash() {
        return Crypto.md5Hash(this.getInsights().stream()
                .map(EventInsight::hashCode)
                .map(String::valueOf)
                .collect(Collectors.joining()));
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.JSON_STYLE)
                .append("now", now)
                .append("internalId", internalId)
                .append("id", id)

Changes to src/main/java/app/coffeetime/models/events/EventAction.java.

1
2
3
4


5
6
7
8
9
10
11
package app.coffeetime.models.events;

import app.coffeetime.util.Markdown;
import com.fasterxml.jackson.annotation.JsonIgnore;



public class EventAction {
    private final int internalId;
    private final String id;
    private final int userId;
    private final String creatorName;
    private final int topicId;




>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
package app.coffeetime.models.events;

import app.coffeetime.util.Markdown;
import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.Objects;

public class EventAction {
    private final int internalId;
    private final String id;
    private final int userId;
    private final String creatorName;
    private final int topicId;
85
86
87
88
89
90
91
92













        sb.append(", action='").append(action).append('\'');
        sb.append(", actionPublic=").append(actionPublic);
        sb.append(", complete=").append(complete);
        sb.append(", ownedByMe=").append(ownedByMe);
        sb.append('}');
        return sb.toString();
    }
}




















|
>
>
>
>
>
>
>
>
>
>
>
>
>
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
        sb.append(", action='").append(action).append('\'');
        sb.append(", actionPublic=").append(actionPublic);
        sb.append(", complete=").append(complete);
        sb.append(", ownedByMe=").append(ownedByMe);
        sb.append('}');
        return sb.toString();
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final EventAction that = (EventAction) o;
        return actionPublic == that.actionPublic && complete == that.complete && Objects.equals(id, that.id) && Objects.equals(action, that.action);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, action, actionPublic, complete);
    }
}

Changes to src/main/java/app/coffeetime/models/events/EventInsight.java.

1
2
3
4


5
6
7
8
9
10
11
package app.coffeetime.models.events;

import app.coffeetime.util.Markdown;
import com.fasterxml.jackson.annotation.JsonIgnore;



public class EventInsight {
    private final int internalId;
    private final String id;
    private final int userId;
    private final String creatorName;
    private final int topicId;




>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
package app.coffeetime.models.events;

import app.coffeetime.util.Markdown;
import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.Objects;

public class EventInsight {
    private final int internalId;
    private final String id;
    private final int userId;
    private final String creatorName;
    private final int topicId;
82
83
84
85
86
87
88
89













        sb.append(", topicId=").append(topicId);
        sb.append(", insight='").append(insight).append('\'');
        sb.append(", insightPublic=").append(insightPublic);
        sb.append(", ownedByMe=").append(ownedByMe);
        sb.append('}');
        return sb.toString();
    }
}




















|
>
>
>
>
>
>
>
>
>
>
>
>
>
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
        sb.append(", topicId=").append(topicId);
        sb.append(", insight='").append(insight).append('\'');
        sb.append(", insightPublic=").append(insightPublic);
        sb.append(", ownedByMe=").append(ownedByMe);
        sb.append('}');
        return sb.toString();
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final EventInsight that = (EventInsight) o;
        return insightPublic == that.insightPublic && Objects.equals(id, that.id) && Objects.equals(insight, that.insight);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, insight, insightPublic);
    }
}

Changes to src/main/java/app/coffeetime/models/events/EventTopic.java.

1
2
3
4
5
6
7


8
9
10
11
12
13
14
package app.coffeetime.models.events;

import app.coffeetime.util.Markdown;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;



public class EventTopic {
    public enum TopicStatus {available, inProcess, processed}

    private final int internalId;
    private final String id;
    private final String topic;
    private final String topicHtml;







>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package app.coffeetime.models.events;

import app.coffeetime.util.Markdown;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;

import java.util.Objects;

public class EventTopic {
    public enum TopicStatus {available, inProcess, processed}

    private final int internalId;
    private final String id;
    private final String topic;
    private final String topicHtml;
103
104
105
106
107
108
109
110













                .append("creatorName", creatorName)
                .append("requesterEmail", requesterEmail)
                .append("requesterVotes", requesterVotes)
                .append("status", status)
                .append("allVotes", allVotes)
                .toString();
    }
}




















|
>
>
>
>
>
>
>
>
>
>
>
>
>
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
                .append("creatorName", creatorName)
                .append("requesterEmail", requesterEmail)
                .append("requesterVotes", requesterVotes)
                .append("status", status)
                .append("allVotes", allVotes)
                .toString();
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final EventTopic that = (EventTopic) o;
        return allVotes == that.allVotes && Objects.equals(id, that.id) && Objects.equals(topic, that.topic) && status == that.status;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, topic, status, allVotes);
    }
}

Changes to src/main/java/app/coffeetime/web/EventRouting.java.

376
377
378
379
380
381
382
383
384
385
386
387
388
389


390
391
392
393
394
395
396
397
398
399
400
401
402



403
404
405
406
407
408
409
410
411
412
413
414

415
416
417
418
419
420
421
422
423
                            return addFlash(super.sessionProperties(), "leave.success");
                        }
                    };
                }));

        router.POST("/event/:eventId/insight", wrapRequireApiUser(userModel, sessionProperties, (email, req) ->
                withLoadedEvent(email, true, eventModel, req, (event, form, eventNotFound) -> {
                    final Optional<String> text = Optional.ofNullable(form.get("text"));
                    return event.isRequesterParticipant() ?
                            text.map(s -> StringUtils.isBlank(s) ? null :
                                            addEventInsight(eventModel, event.getId(), email, req.timeZone(), s))
                                    .orElse(plainResponse(HttpResponseStatus.BAD_REQUEST,
                                            "No insight text given")) :
                            eventNotFound.get();


                })));

        router.DELETE("/event/:eventId/insight/:insightId", wrapRequireApiUser(userModel, sessionProperties, (email, request) ->
        {
            final String insightId = request.requestPropertyAsString("insightId");
            try {
                eventModel.deleteInsight(email, insightId);
                return success();
            } catch (UserFriendlyRuntimeException e) {
                LOG.get().error("Unable to delete insight", e);
                return plainResponse(toHttpResponse(e), e.getUserFriendlyMessage());
            }
        }));




        router.POST("/event/:eventId/insight/:insightId/public", wrapRequireApiUser(userModel, sessionProperties, (email, req) ->
                withLoadedEvent(email, true, eventModel, req, (event, form, eventNotFound) -> {
                    final boolean shouldBePublic = Optional.ofNullable(form.get("public"))
                            .map(Boolean::parseBoolean).orElseThrow();
                    final String insightId = req.requestPropertyAsString("insightId");
                    return event.getInsights()
                            .stream()
                            .filter(eventAction -> eventAction.getId().equals(insightId))
                            .findFirst()
                            .map(eventAction -> {
                                eventModel.setInsightAccess(email, insightId, shouldBePublic);

                                return success();
                            }).orElseGet(eventNotFound);
                })));

        router.POST("/event/:eventId/nextStep", wrapRequireApiUser(userModel, sessionProperties, (email, req) ->
                withLoadedEvent(email, true, eventModel, req, (event, form, eventNotFound) -> {
                    final Optional<String> text = Optional.ofNullable(form.get("text"));
                    return event.isRequesterParticipant() ?
                            text.map(s -> StringUtils.isBlank(s) ? null :







|
|
<
|
|
|
|
>
>


|




|


|


>
>
>






|

|

<
|
>
|
<







376
377
378
379
380
381
382
383
384

385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416

417
418
419

420
421
422
423
424
425
426
                            return addFlash(super.sessionProperties(), "leave.success");
                        }
                    };
                }));

        router.POST("/event/:eventId/insight", wrapRequireApiUser(userModel, sessionProperties, (email, req) ->
                withLoadedEvent(email, true, eventModel, req, (event, form, eventNotFound) -> {
                    final Optional<String> text = Optional.ofNullable(form.get("insight"));
                    if (event.isRequesterParticipant()) {

                        text.filter(StringUtils::isNotBlank).ifPresent(s -> eventModel.addInsight(event.getId(), email, req.timeZone(), s));
                    } else {
                        LOG.get().warn(format("User '%s' requested to add insight to event '%s' but they aren't a participant",
                                email, event.getId()));
                    }
                    return eventInsightsFragment(mergeTool, eventModel, req);
                })));

        router.POST("/event/:eventId/insight/:insightId/delete", wrapRequireApiUser(userModel, sessionProperties, (email, request) ->
        {
            final String insightId = request.requestPropertyAsString("insightId");
            try {
                eventModel.deleteInsight(email, insightId);
                return eventInsightsFragment(mergeTool, eventModel, request);
            } catch (UserFriendlyRuntimeException e) {
                LOG.get().error("Unable to delete insight", e);
                return redirect(toEvent(request), e.getUserFriendlyMessage());
            }
        }));

        router.GET("/event/:eventId/insightsFragment", wrapAuth(userModel, sessionProperties,
                req -> eventInsightsFragment(mergeTool, eventModel, req)));

        router.POST("/event/:eventId/insight/:insightId/public", wrapRequireApiUser(userModel, sessionProperties, (email, req) ->
                withLoadedEvent(email, true, eventModel, req, (event, form, eventNotFound) -> {
                    final boolean shouldBePublic = Optional.ofNullable(form.get("public"))
                            .map(Boolean::parseBoolean).orElseThrow();
                    final String insightId = req.requestPropertyAsString("insightId");
                    event.getInsights()
                            .stream()
                            .filter(insight -> insight.getId().equals(insightId))
                            .findFirst()

                            .ifPresentOrElse(insight -> eventModel.setInsightAccess(email, insightId, shouldBePublic),
                                    () -> LOG.get().warn(format("Can't make missing insight '%s' public", insightId)));
                    return eventInsightsFragment(mergeTool, eventModel, req);

                })));

        router.POST("/event/:eventId/nextStep", wrapRequireApiUser(userModel, sessionProperties, (email, req) ->
                withLoadedEvent(email, true, eventModel, req, (event, form, eventNotFound) -> {
                    final Optional<String> text = Optional.ofNullable(form.get("text"));
                    return event.isRequesterParticipant() ?
                            text.map(s -> StringUtils.isBlank(s) ? null :
689
690
691
692
693
694
695









696
697
698
699
700
701
702
                    if (flagIs("MODE", "dev")) {
                        eventModel.forceToEnd(email, req.timeZone(), eventId);
                        return success();
                    }
                    return redirect("/", "Access denied");
                }));
    }










    @NotNull
    private static Response emailForm(final Request req,
                                      final String email,
                                      final Event event,
                                      final MergeTool mergeTool,
                                      final String template,







>
>
>
>
>
>
>
>
>







692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
                    if (flagIs("MODE", "dev")) {
                        eventModel.forceToEnd(email, req.timeZone(), eventId);
                        return success();
                    }
                    return redirect("/", "Access denied");
                }));
    }

    private static Response eventInsightsFragment(final MergeTool mergeTool, final EventModel eventModel, final Request req) {
        return withLoadedEvent(req.principal().orElseThrow(), false,
                eventModel,
                req,
                (event, eventNotFound) ->
                        htmlResponse(renderFile(mergeTool, req, "/templates/web/eventFragments/insightList.vm",
                                TreePMap.singleton("event", event))));
    }

    @NotNull
    private static Response emailForm(final Request req,
                                      final String email,
                                      final Event event,
                                      final MergeTool mergeTool,
                                      final String template,
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
    @NotNull
    private static HttpResponseStatus translateContentResult(final EventModel.EventContentResult result) {
        return result.status.isSuccess() ? HttpResponseStatus.OK :
                result.status.isMissing() ? HttpResponseStatus.NOT_FOUND :
                        HttpResponseStatus.BAD_REQUEST;
    }

    private static Response addEventInsight(final EventModel eventModel,
                                            final String eventId,
                                            final String email,
                                            final TimeZone timeZone,
                                            final String insight) {
        final EventModel.EventContentResult result = eventModel.addInsight(eventId, email, timeZone, insight);
        return plainResponse(translateContentResult(result), result.contentId);
    }

    private static Response addEventNextStep(final EventModel eventModel,
                                             final String eventId,
                                             final String email,
                                             final TimeZone timeZone,
                                             final String nextStep) {
        final EventModel.EventContentResult result = eventModel.addNextStep(eventId, email, timeZone, nextStep);
        return plainResponse(translateContentResult(result), result.contentId);







<
<
<
<
<
<
<
<
<







1133
1134
1135
1136
1137
1138
1139









1140
1141
1142
1143
1144
1145
1146
    @NotNull
    private static HttpResponseStatus translateContentResult(final EventModel.EventContentResult result) {
        return result.status.isSuccess() ? HttpResponseStatus.OK :
                result.status.isMissing() ? HttpResponseStatus.NOT_FOUND :
                        HttpResponseStatus.BAD_REQUEST;
    }










    private static Response addEventNextStep(final EventModel eventModel,
                                             final String eventId,
                                             final String email,
                                             final TimeZone timeZone,
                                             final String nextStep) {
        final EventModel.EventContentResult result = eventModel.addNextStep(eventId, email, timeZone, nextStep);
        return plainResponse(translateContentResult(result), result.contentId);

Changes to src/main/js/index.ts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export * from './charting';
export * from './chatBox';
export * from './chatMsg';
export * from './crypto';
export * from './currentSpeaker';
export * from './eventApi';
export * from './helloWorld';
export * from './insight';
export * from './insightsList';
export * from './nextStep';
export * from './nextStepsList';
export * from './participantList';
export * from './testing';
export * from './textUtil';
export * from './topic';
export * from './topicsList';







<
<







1
2
3
4
5
6
7


8
9
10
11
12
13
14
export * from './charting';
export * from './chatBox';
export * from './chatMsg';
export * from './crypto';
export * from './currentSpeaker';
export * from './eventApi';
export * from './helloWorld';


export * from './nextStep';
export * from './nextStepsList';
export * from './participantList';
export * from './testing';
export * from './textUtil';
export * from './topic';
export * from './topicsList';

Deleted src/main/js/insight.ts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import {html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
import {Component} from "./component";

@customElement('insight-info')
class Insight extends Component {
    @property() id: string = null
    @property() insightHtml: string = null
    @property({type: Boolean}) isPublic: boolean = null
    @property({type: Boolean}) ownedByMe: boolean = null
    @property() creatorName: string = null

    render() {
        return html`
            <li id="insight-${this.id}">
                <div class="context-item-text">
                    ${this.ownedByMe ?
                            html`<access-btn id=${this.id} entityType="insight" ?isPublic=${this.isPublic}/>` :
                            html`<span class="context-access"></span>`}
                    <div class="context-item-info">
                        <div class="context-item-description">${unsafeHTML(this.insightHtml)}</div>
                        ${this.ownedByMe ?
                                html`<span></span>` :
                                html`<span class="context-item-creator">${this.creatorName}</span>`}
                    </div>
                    ${this.ownedByMe ?
                            html`<delete-btn id=${this.id} entityType="insight"/>` :
                            html`<span class="context-action"></span>`
                    }
                </div>
            </li>
        `;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































































Deleted src/main/js/insightsList.ts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import {html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import * as R from 'ramda';
import {Component} from "./component";

@customElement('insights-list')
class InsightsList extends Component {
    @property() insights: {
        [key: string]: {
            id: string,
            insight: string,
            insightHtml: string,
            insightPublic: boolean,
            ownedByMe: boolean,
            creatorName: string
        }
    }[];

    constructor() {
        super();
        this.insights = [];
        this.addEventListener('insights-refreshed', (e: CustomEvent) => {
            e.preventDefault();
            const insights = e.detail;
            const insightIds = new Set();
            insights.forEach((insight) => {
                if (insight.ownedByMe || insight.insightPublic) {
                    this.insights[insight.id] = insight;
                    insightIds.add(insight.id);
                }
            });

            Object.keys(this.insights).forEach(k => {
                if (!insightIds.has(k)) {
                    delete this.insights[k];
                }
            });
            this.requestUpdate();
        });
    }

    render() {
        const ks: string[] = Object.keys(this.insights);
        return html`
            <ul id="insights-list" class="collapsible context-item-list">
                ${R.map((k) => {
                            const {id, insightHtml, insightPublic, ownedByMe, creatorName} = this.insights[k]
                            return html`
                                <insight-info id=${id}
                                              insightHtml=${insightHtml}
                                              ?isPublic=${insightPublic}
                                              ?ownedByMe=${ownedByMe}
                                              creatorName=${creatorName}></insight-info>`;
                        },
                        R.reverse(ks))}
            </ul>
        `;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































Changes to src/main/resources/templates/web/event.vm.

304
305
306
307
308
309
310




311
312

313
314

315
316
317

318

319


320
321




322
323
324
325

326
327
328
329
330
331
332

            <div class="context-section">
                <div class="heading-wrapper">
                    <h2>Insights</h2>
                </div>
                <em class="section-hint">We'll send you a list after the
                    event</em>




                <insights-list></insights-list>


                <form id="add-insight"
                      class="add-item-row"

                      method="post"
                      action="#"
                      onsubmit="return serverAddInsight()">

                    <input id="new-insight" class="add-content-element"

                           disabled="disabled" type="text"


                           placeholder="I learned..."
                           autocomplete="off" maxlength="200"/>




                    <div class="new-item-btns">

                        <button type="submit" class="add-content-element"
                                disabled="disabled" id="new-insight-btn">add

                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>








>
>
>
>
|
|
>


>

|
|
>
|
>
|
>
>

|
>
>
>
>

|
|
|
>







304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347

            <div class="context-section">
                <div class="heading-wrapper">
                    <h2>Insights</h2>
                </div>
                <em class="section-hint">We'll send you a list after the
                    event</em>
                <a href="/event/${event.id}/insightsFragment"
                   id="insights-reload"
                   ts-req
                   ts-req-method="get"
                   ts-target="#insights-list"
                   style="display: none">reload</a>
                #parse("/templates/web/eventFragments/insightList.vm")
                <form id="add-insight"
                      class="add-item-row"
                      action="/event/${event.id}/insight"
                      method="post"
                      ts-req=""
                      ts-target="#insights-list"
                      ts-req-after="target '#new-insight', attr value ''">
                    <input id="new-insight"
                           class="add-content-element"
                           disabled="disabled"
                           type="text"
                           name="insight"
                           placeholder="I learned..."
                           autocomplete="off"
                           maxlength="200"/>
                    <input type="hidden"
                           name="${csrfTokenName}"
                           value="${csrfToken}"/>
                    <div class="new-item-btns">
                        <button type="submit"
                                class="add-content-element"
                                disabled="disabled"
                                id="new-insight-btn">add
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>

386
387
388
389
390
391
392

393
394
395
396
397
398
399
    let videoSize = 0;

    //Don't play the gong on first page load
    let roundCompleted = true;
    let participantCount = 0;
    let currentTopic = null;
    let currentTopicId = null;


        #if(${event.standardVoting})
        let continuationVote = null;
        let allContinuationVotes = {};
        #end

    let participants = [];







>







401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
    let videoSize = 0;

    //Don't play the gong on first page load
    let roundCompleted = true;
    let participantCount = 0;
    let currentTopic = null;
    let currentTopicId = null;
    let insightHash = "${event.insightHash}";

        #if(${event.standardVoting})
        let continuationVote = null;
        let allContinuationVotes = {};
        #end

    let participants = [];
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
        return serverAddItem("new-next-step", "new-next-step-btn", "/event/${event.id}/nextStep",
                "text", () => refreshEvent(false),
                () => {
                    alert("Unable to add next step at this time. Please try again later.");
                });
    }

    function serverAddInsight() {
        return serverAddItem("new-insight", "new-insight-btn", "/event/${event.id}/insight",
                "text", () => refreshEvent(false),
                () => {
                    alert("Unable to add insight at this time. Please try again later.");
                });
    }

    function sendStartTopic(topicId) {
        const req = new XMLHttpRequest();

        req.onreadystatechange = function () {
            if (req.readyState === XMLHttpRequest.DONE) {
                if (req.status === 200) {
                    refreshEvent(false);







<
<
<
<
<
<
<
<







456
457
458
459
460
461
462








463
464
465
466
467
468
469
        return serverAddItem("new-next-step", "new-next-step-btn", "/event/${event.id}/nextStep",
                "text", () => refreshEvent(false),
                () => {
                    alert("Unable to add next step at this time. Please try again later.");
                });
    }









    function sendStartTopic(topicId) {
        const req = new XMLHttpRequest();

        req.onreadystatechange = function () {
            if (req.readyState === XMLHttpRequest.DONE) {
                if (req.status === 200) {
                    refreshEvent(false);
567
568
569
570
571
572
573

574
575

576
577
578
579
580
581
582
            if (currentTopic || eventStatus === 'inProgress') {
                R.forEach((roundTimeE) => {
                    roundTimeE.innerText = roundDurationRemaining;
                }, roundTimeElements);
            }
        #end


        document.querySelector("insights-list").dispatchEvent(new CustomEvent("insights-refreshed",
                {detail: event.insights}));


        document.querySelector("next-steps-list").dispatchEvent(new CustomEvent("next-steps-refreshed",
                {detail: event.actions}));

        document.querySelector("topics-list").dispatchEvent(new CustomEvent("topics-refreshed",
                {
                    detail: {







>
|
|
>







575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
            if (currentTopic || eventStatus === 'inProgress') {
                R.forEach((roundTimeE) => {
                    roundTimeE.innerText = roundDurationRemaining;
                }, roundTimeElements);
            }
        #end

        if (event.insightHash !== insightHash) {
            document.getElementById("insights-reload").click();
            insightHash = event.insightHash;
        }

        document.querySelector("next-steps-list").dispatchEvent(new CustomEvent("next-steps-refreshed",
                {detail: event.actions}));

        document.querySelector("topics-list").dispatchEvent(new CustomEvent("topics-refreshed",
                {
                    detail: {

Added src/main/resources/templates/web/eventFragments/insightList.vm.

























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<ul id="insights-list" class="collapsible context-item-list">
    <script>insightHash = "${event.insightHash}";</script>
    #foreach($insight in $event.insights)
        #if(${insight.insightPublic} || ${insight.ownedByMe})
            <li id="insight-${insight.id}">
                <div class="context-item-text">
                    #if(${insight.ownedByMe})
                        <form id="updateAccessForm"
                              method="post"
                              action="/event/${event.id}/insight/${insight.id}/public"
                              ts-req=""
                              ts-target="#insights-list">
                            <button name="public"
                                    type="submit"
                                    class="action material-icons context-action"
                                    type="button">#if(${insight.insightPublic})
                                &#xe8f4;#else&#xe8f5;#end</button>
                            #set($newValue = ! ${insight.insightPublic})
                            <input type="hidden"
                                   name="public"
                                   value="${newValue}"/>
                            <input type="hidden"
                                   name="${csrfTokenName}"
                                   value="${csrfToken}"/>
                        </form>
                    #else
                        <span class="context-access"></span>
                    #end
                    <div class="context-item-info">
                        <div class="context-item-description">
                            ${insight.insightHtml}
                        </div>
                        #if(${insight.ownedByMe})
                            <span></span>
                        #else
                            <span class="context-item-creator">${insight.creatorName}</span>
                        #end
                    </div>
                    #if(${insight.ownedByMe})
                        <form id="updateAccessForm"
                              method="post"
                              action="/event/${event.id}/insight/${insight.id}/delete"
                              ts-req=""
                              ts-target="#insights-list"
                              ts-req-before="confirm 'Delete this insight?'">
                            <button name="public"
                                    type="submit"
                                    class="action material-icons context-action"
                                    type="button">&#xe872;</button>
                            <input type="hidden"
                                   name="${csrfTokenName}"
                                   value="${csrfToken}"/>
                        </form>
                    #end
                </div>
            </li>
        #end

    #end
</ul>