Skip to content

Commit 74802cf

Browse files
committed
add goal to room
1 parent 4bf9928 commit 74802cf

File tree

4 files changed

+194
-15
lines changed

4 files changed

+194
-15
lines changed

src/main/java/sh/mob/timer/web/Room.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,41 @@ final class Room {
2424
private final List<TimerRequest> timerRequests = new CopyOnWriteArrayList<>();
2525
private final Sinks.Many<TimerRequest> sink =
2626
Sinks.many().replay().latestOrDefault(NULL_TIMER_REQUEST);
27+
private Goal currentGoal = Goal.NO_GOAL;
28+
29+
private final Sinks.Many<Goal> goalRequestSink =
30+
Sinks.many().replay().latestOrDefault(Goal.NO_GOAL);
2731

2832
Room(String name) {
2933
this.name = name;
3034
}
3135

32-
public void add(Long timer, String user, Instant requested) {
36+
public void addTimer(Long timer, String user, Instant requested) {
3337
var nextUser = findNextUser(user);
3438
var timerRequest = new TimerRequest(timer, requested, user, nextUser, TimerType.TIMER);
3539
timerRequests.add(timerRequest);
3640
sink.tryEmitNext(timerRequest);
3741
}
3842

43+
public void setGoal(String text, String user, Instant requested) {
44+
var newGoal = new Goal(text, requested, user);
45+
currentGoal = newGoal ;
46+
goalRequestSink.tryEmitNext(newGoal);
47+
}
48+
49+
public void deleteGoal(String user) {
50+
if(currentGoal != Goal.NO_GOAL){
51+
currentGoal = Goal.NO_GOAL;
52+
goalRequestSink.tryEmitNext(Goal.NO_GOAL);
53+
log.info(
54+
"Delete current goal by user {} for room {}",
55+
user,
56+
name);
57+
} else {
58+
log.info("Try to delete current goal by user {} for room {}, but there is no current goal.", user, name);
59+
}
60+
}
61+
3962
private String findNextUser(String user) {
4063
if (timerRequests.isEmpty()) {
4164
return null;
@@ -73,10 +96,14 @@ public void addBreaktimer(Long breaktimer, String user) {
7396
sink.tryEmitNext(timerRequest);
7497
}
7598

76-
public Sinks.Many<TimerRequest> sink() {
99+
public Sinks.Many<TimerRequest> timerRequestSink() {
77100
return sink;
78101
}
79102

103+
public Sinks.Many<Goal> goalRequestSink() {
104+
return goalRequestSink;
105+
}
106+
80107
Optional<TimerRequest> lastTimerRequest() {
81108
if (timerRequests.isEmpty()) {
82109
return Optional.empty();
@@ -98,6 +125,10 @@ public String name() {
98125
return name;
99126
}
100127

128+
public Goal currentGoal() {
129+
return currentGoal;
130+
}
131+
101132
public List<TimerRequest> historyWithoutLatest() {
102133
if (timerRequests.isEmpty()) {
103134
return List.of();
@@ -117,6 +148,10 @@ private static boolean isTimerActive(TimerRequest timerRequest, Instant now) {
117148
&& timerRequest.getRequested().plus(timerRequest.getTimer(), MINUTES).isAfter(now);
118149
}
119150

151+
public record Goal(String goal, Instant requested, String user){
152+
public static final Goal NO_GOAL = new Goal(null, null, null);
153+
}
154+
120155
public static final class TimerRequest {
121156

122157
enum TimerType {

src/main/java/sh/mob/timer/web/RoomApiController.java

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,10 @@
88
import org.slf4j.LoggerFactory;
99
import org.springframework.http.HttpStatus;
1010
import org.springframework.http.MediaType;
11+
import org.springframework.http.ResponseEntity;
1112
import org.springframework.http.codec.ServerSentEvent;
1213
import org.springframework.http.server.reactive.ServerHttpResponse;
13-
import org.springframework.web.bind.annotation.GetMapping;
14-
import org.springframework.web.bind.annotation.PathVariable;
15-
import org.springframework.web.bind.annotation.PutMapping;
16-
import org.springframework.web.bind.annotation.RequestBody;
17-
import org.springframework.web.bind.annotation.RequestMapping;
18-
import org.springframework.web.bind.annotation.ResponseStatus;
19-
import org.springframework.web.bind.annotation.RestController;
14+
import org.springframework.web.bind.annotation.*;
2015
import reactor.core.publisher.Flux;
2116
import sh.mob.timer.web.Room.TimerRequest;
2217

@@ -50,11 +45,17 @@ public Flux<ServerSentEvent<Object>> getEventStream(
5045
var room = roomRepository.get(roomId);
5146

5247
var timerRequestFlux =
53-
room.sink()
48+
room.timerRequestSink()
5449
.asFlux()
5550
.map(
5651
timerRequest ->
5752
ServerSentEvent.builder().event("TIMER_REQUEST").data(timerRequest).build());
53+
var goalRequestFlux =
54+
room.goalRequestSink()
55+
.asFlux()
56+
.map(
57+
goalRequest ->
58+
ServerSentEvent.builder().event("GOAL_REQUEST").data(goalRequest).build());
5859
var keepAliveFlux =
5960
Flux.interval(Duration.ofSeconds(5L))
6061
.map(
@@ -67,7 +68,7 @@ public Flux<ServerSentEvent<Object>> getEventStream(
6768
Flux.just(room.historyWithoutLatest())
6869
.map(list -> ServerSentEvent.builder().event("INITIAL_HISTORY").data(list).build());
6970

70-
return Flux.concat(initialHistory, keepAliveFlux.mergeWith(timerRequestFlux));
71+
return Flux.concat(initialHistory, keepAliveFlux.mergeWith(timerRequestFlux).mergeWith(goalRequestFlux));
7172
}
7273

7374
@PutMapping("/{roomId:[A-Za-z0-9-_]+}")
@@ -76,7 +77,7 @@ public void publishEvent(@PathVariable String roomId, @RequestBody PutTimerReque
7677
var room = roomRepository.get(roomId);
7778
if (timerRequest.timer() != null) {
7879
long timer = truncateTooLongTimers(timerRequest.timer());
79-
room.add(
80+
room.addTimer(
8081
timer, timerRequest.user(), Instant.now(clock));
8182
log.info(
8283
"Add timer {} by user {} for room {}",
@@ -98,6 +99,38 @@ public void publishEvent(@PathVariable String roomId, @RequestBody PutTimerReque
9899
}
99100
}
100101

102+
@PutMapping("/{roomId:[A-Za-z0-9-_]+}/goal")
103+
@ResponseStatus(HttpStatus.ACCEPTED)
104+
public void putGoal(@PathVariable String roomId, @RequestBody PutGoalRequest goalRequest) {
105+
var room = roomRepository.get(roomId);
106+
if (goalRequest.goal() != null) {
107+
String goal = truncateTooLongGoal(goalRequest.goal());
108+
room.setGoal(
109+
goal, goalRequest.user(), Instant.now(clock));
110+
log.info(
111+
"Add goal \"{}\" by user {} for room {}",
112+
goalRequest.goal(),
113+
goalRequest.user(),
114+
room.name());
115+
} else {
116+
log.warn("Could not understand PUT goal request for room {}", roomId);
117+
}
118+
}
119+
120+
@DeleteMapping("/{roomId:[A-Za-z0-9-_]+}/goal")
121+
@ResponseStatus(HttpStatus.ACCEPTED)
122+
public void putGoal(@PathVariable String roomId, @RequestBody DeleteGoalRequest deleteGoalRequest) {
123+
var room = roomRepository.get(roomId);
124+
room.deleteGoal(deleteGoalRequest.user());
125+
}
126+
127+
@GetMapping("/{roomId:[A-Za-z0-9-_]+}/goal")
128+
@ResponseStatus(HttpStatus.ACCEPTED)
129+
public ResponseEntity<GoalResponse> getGoal(@PathVariable String roomId) {
130+
var room = roomRepository.get(roomId);
131+
return ResponseEntity.ofNullable(GoalResponse.of(room.currentGoal()));
132+
}
133+
101134
private void incrementBreakTimerStatsExceptForTestRoom(Room room, long breaktimer) {
102135
if (!Objects.equals(room.name(), SMOKETEST_ROOM_NAME)) {
103136
stats.incrementBreaktimer(breaktimer);
@@ -114,6 +147,18 @@ private static long truncateTooLongTimers(Long timer) {
114147
return Math.min(60 * 24, Math.max(0, timer));
115148
}
116149

150+
private static String truncateTooLongGoal(String goal) {
151+
return goal.length() > 256 ? goal.substring(0,256-1-3) + "...": goal;
152+
}
153+
154+
public record GoalResponse(String goal){
155+
public static GoalResponse of(Room.Goal goal){
156+
return new GoalResponse(goal.goal());
157+
}
158+
}
159+
160+
public record PutGoalRequest(String goal, String user){}
161+
public record DeleteGoalRequest(String user){}
117162
static final class PutTimerRequest {
118163

119164
private final Long timer;

src/main/java/sh/mob/timer/web/RoomRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public long count() {
5151

5252
public long countConnections() {
5353
return repository.values().stream()
54-
.mapToLong(room -> room.sink().currentSubscriberCount())
54+
.mapToLong(room -> room.timerRequestSink().currentSubscriberCount())
5555
.sum();
5656
}
5757

src/main/resources/templates/room.html

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555

5656
<main class="container">
5757

58-
<div class="col-lg-8 mx-auto p-3 py-md-5">
58+
<div class="col-lg-12 mx-auto p-3 py-md-5">
5959

6060
<div class="text-center">
6161
<a class="text-decoration-none mb-3" href="/"><h1>Mob Timer</h1></a>
@@ -71,6 +71,8 @@
7171
<span id="timer-next-user" class="text-muted" title="next person"></span>
7272
</div>
7373

74+
<div id="goal"></div>
75+
7476
<div>
7577
<div class="mt-3" style="justify-content: center">
7678
<div class="btn-group">
@@ -108,13 +110,43 @@
108110
</ul>
109111
</div>
110112

113+
<!--<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#goalModal">🎯</button>-->
114+
<div class="btn-group">
115+
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">🎯</button>
116+
<ul class="dropdown-menu">
117+
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#goalModal">Set new goal</a></li>
118+
<li><a class="dropdown-item" onclick="deleteGoal()">Delete current goal</a></li>
119+
</ul>
120+
</div>
121+
111122
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#configurationModal">⚙️</button>
112123
</div>
113124
</div>
114125

126+
<!-- Goal Modal -->
127+
<div class="modal fade" id="goalModal" tabindex="-1" role="dialog" aria-labelledby="goalModalLabel" aria-hidden="true">
128+
<div class="modal-dialog" role="document">
129+
<div class="modal-content">
130+
<div class="modal-header">
131+
<h5 class="modal-title" id="goalModalLabel">Set a goal for your mob session</h5>
132+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="goalModalClose"></button>
133+
</div>
134+
<div class="modal-body">
135+
<div class="input-group mb-3">
136+
<span class="input-group-text">Set your goal</span>
137+
<input type="text" class="form-control" placeholder="goal" id="goal-input">
138+
</div>
139+
</div>
140+
<div class="modal-footer">
141+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
142+
<button type="button" class="btn btn-primary" onclick="saveGoal()">Save goal</button>
143+
</div>
144+
</div>
145+
</div>
146+
</div>
115147

116148

117-
<!-- Modal -->
149+
<!-- Setting Modal -->
118150
<div class="modal fade" id="configurationModal" tabindex="-1" role="dialog" aria-labelledby="configurationModalLabel" aria-hidden="true">
119151
<div class="modal-dialog" role="document">
120152
<div class="modal-content">
@@ -234,6 +266,11 @@ <h5>Integration with the mob tool</h5>
234266
: date.toLocaleString();
235267
}
236268

269+
let goalDiv = document.getElementById('goal');
270+
function updateGoal(data) {
271+
goalDiv.innerText = data !== null ? `🎯 ${data}` : '';
272+
}
273+
237274
let timerDiv = document.getElementById('timer');
238275
function updateRemainingTime(data) {
239276
document.title = `${data} #${room}`;
@@ -320,6 +357,7 @@ <h5>Integration with the mob tool</h5>
320357
let requestUser = null;
321358
let requestNextUser = null;
322359
let requestTimer = null;
360+
let goal = null;
323361

324362

325363
function renderTimer() {
@@ -495,6 +533,25 @@ <h5>Integration with the mob tool</h5>
495533
prependToHistory(timerRequest);
496534
});
497535

536+
eventSource.addEventListener('GOAL_REQUEST', (event) => {
537+
console.log('handling event GOAL_REQUEST ' + event.data);
538+
let goalRequest = JSON.parse(event.data);
539+
let requested = goalRequest["requested"];
540+
let newGoal = goalRequest["goal"];
541+
let user = goalRequest["user"];
542+
543+
if (newGoal === null && user === null) {
544+
console.log('Resetting state');
545+
546+
goal = null;
547+
updateGoal(null);
548+
return;
549+
}
550+
551+
updateGoal(newGoal)
552+
goal = newGoal;
553+
});
554+
498555
checkAudioPlayback();
499556

500557
function sendTimer(type, timer, user) {
@@ -509,7 +566,49 @@ <h5>Integration with the mob tool</h5>
509566
xhr.send(json);
510567
}
511568

569+
function saveGoal() {
570+
goalInput = document.getElementById('goal-input');
571+
goalModalClose = document.getElementById('goalModalClose');
572+
if(goalInput.value !== ""){
573+
sendGoal(goalInput.value)
574+
goalInput.value = ""
575+
goalModalClose.click();
576+
} else {
577+
console.log("Did not save goal as goal was empty.")
578+
}
579+
}
580+
581+
function sendGoal(goal) {
582+
let method = "PUT";
583+
let url = "/" + room + "/goal";
584+
const user = localStorage.getItem('user');
585+
let json = `{"goal": "${goal}","user": "${user}"}`;
512586

587+
console.log(`${method} ${url} ${json}`)
588+
var xhr = new XMLHttpRequest();
589+
xhr.open(method, url);
590+
xhr.setRequestHeader("Content-Type", "application/json");
591+
xhr.send(json);
592+
}
593+
594+
function deleteGoal(){
595+
goalModalClose = document.getElementById('goalModalClose');
596+
sendDeleteGoal();
597+
goalModalClose.click();
598+
}
599+
600+
function sendDeleteGoal() {
601+
let method = "DELETE";
602+
let url = "/" + room + "/goal";
603+
let user = localStorage.getItem('user');
604+
let json = `{"user": "${user}"}`;
605+
606+
console.log(`${method} ${url} ${json}`)
607+
var xhr = new XMLHttpRequest();
608+
xhr.open(method, url);
609+
xhr.setRequestHeader("Content-Type", "application/json");
610+
xhr.send(json);
611+
}
513612

514613
function syncAndSetInitialValue(inputElementId, storageId, defaultValue, onChange) {
515614
const callOnChange = () => {

0 commit comments

Comments
 (0)