Settings page, Avatars, Displaynames, pronouns

This commit is contained in:
Michael Wain 2024-02-26 03:50:52 +03:00
parent 064ded5fe5
commit fbcaa886c1
18 changed files with 147 additions and 34 deletions

View File

@ -7,6 +7,7 @@ import com.alterdekim.game.component.result.LongPollResultType;
import com.alterdekim.game.dto.ChatResult;
import com.alterdekim.game.dto.UserResult;
import com.alterdekim.game.entities.Chat;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.ChatServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.StringUtil;
@ -30,7 +31,9 @@ public class ChatProcessor extends Processor<ChatResult> {
return new LongPollResultSingle<>(getType(), results.stream()
.peek(c -> c.setMessage(StringUtil.escapeTags(getParent().getUserService(), c.getMessage())))
.map(c -> new ChatResult(c, new UserResult( c.getUserId(), getParent().getUserService().findById(c.getUserId()).getUsername())))
.collect(Collectors.toList()));
.map(c -> {
User u1 = getParent().getUserService().findById(c.getUserId());
return new ChatResult(c, new UserResult( c.getUserId(), u1.getUsername(), u1.getAvatarId()));
}).collect(Collectors.toList()));
}
}

View File

@ -61,7 +61,7 @@ public class FriendProcessor extends Processor<FriendResult> {
private List<UserResult> friends2Users(List<Long> friendIds) {
return friendIds.stream()
.map(id -> getParent().getUserService().findById(id))
.map(u -> new UserResult(u.getId(), u.getUsername()))
.map(u -> new UserResult(u.getId(), u.getDisplayName(), u.getAvatarId()))
.filter(f -> getParent().getMap().keySet().stream().anyMatch(l -> l.longValue() == f.getId().longValue()))
.collect(Collectors.toList());
}

View File

@ -62,7 +62,7 @@ public class RoomProcessor extends Processor<RoomResultV2> {
return rooms.stream()
.map( r -> new RoomResult(r.getId(), r.getPlayerCount(), getParent().getRoomPlayerService().findByRoomId(r.getId()).stream()
.map(p -> getParent().getUserService().findById(p.getUserId()))
.map(p -> new UserResult(p.getId(), p.getUsername()))
.map(p -> new UserResult(p.getId(), p.getDisplayName(), p.getAvatarId()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
}
@ -71,7 +71,7 @@ public class RoomProcessor extends Processor<RoomResultV2> {
return rooms.stream()
.map( r -> new RoomResultV2(RoomResultState.ADD_CHANGE, r.getId(), r.getPlayerCount(), getParent().getRoomPlayerService().findByRoomId(r.getId()).stream()
.map(p -> getParent().getUserService().findById(p.getUserId()))
.map(p -> new UserResult(p.getId(), p.getUsername()))
.map(p -> new UserResult(p.getId(), p.getDisplayName(), p.getAvatarId()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
}

View File

@ -63,7 +63,10 @@ public class APIController {
List<Chat> results = chatService.getLastChats(count);
return ResponseEntity.ok(results.stream()
.peek(c -> c.setMessage(StringUtil.escapeTags(userService, c.getMessage())))
.map(c -> new ChatResult(c, new UserResult( c.getUserId(), userService.findById(c.getUserId()).getUsername())))
.map(c -> {
User u1 = userService.findById(c.getUserId());
return new ChatResult(c, new UserResult( c.getUserId(), u1.getUsername(), u1.getAvatarId()));
})
.collect(Collectors.toList()));
}
@ -156,6 +159,16 @@ public class APIController {
return ResponseEntity.ok().build();
}
@PostMapping("/api/v1/settings/profile/save")
public ResponseEntity<String> saveProfile( @RequestParam("display_name") String displayName,
@RequestParam("nickname") String nickname,
@RequestParam("pronouns") String pronouns ) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = userService.findByUsername(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername()).getId();
userService.updateProfileInfo(userId, displayName, nickname, pronouns);
return ResponseEntity.ok().build();
}
@PostMapping("/async/notify/get/")
@ResponseBody
public DeferredResult<LongPollResult> getNotify(@RequestParam("last_chat_id") Long last_chat_id,

View File

@ -15,15 +15,7 @@ import org.springframework.web.server.ResponseStatusException;
class ImageController {
@Autowired
ImageRepository imageRepository;
@PostMapping(value = "/image/upload/")
private Long uploadImage(@RequestParam MultipartFile multipartImage) throws Exception {
Image dbImage = new Image();
dbImage.setContent(multipartImage.getBytes());
return imageRepository.save(dbImage)
.getId();
}
private ImageRepository imageRepository;
@GetMapping(value = "/image/store/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
Resource downloadImage(@PathVariable Long imageId) {

View File

@ -2,7 +2,9 @@ package com.alterdekim.game.controller;
import com.alterdekim.game.dto.AuthApiObject;
import com.alterdekim.game.dto.FriendPageResult;
import com.alterdekim.game.entities.Image;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.repository.ImageRepository;
import com.alterdekim.game.service.FriendServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.AuthenticationUtil;
@ -14,6 +16,9 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Controller
@ -25,6 +30,9 @@ public class StaticController {
@Autowired
private FriendServiceImpl friendService;
@Autowired
private ImageRepository imageRepository;
@GetMapping("/rules")
public String rulesPage(Model model) {
AuthenticationUtil.authProfile(model, userService);
@ -53,7 +61,7 @@ public class StaticController {
Long userId = AuthenticationUtil.authProfile(model, userService).getId();
model.addAttribute("friends", friendService.getFriendsOfUserId(userId).stream()
.map(l -> userService.findById(l))
.map(u -> new FriendPageResult("/profile/"+u.getId(), "background-image: url(&quot;https://i.dogecdn.wtf/wxEpEiovduvcXadQ&quot;);", u.getUsername(), u.getId())));
.map(u -> new FriendPageResult("/profile/"+u.getId(), "background-image: url(\"/image/store/"+u.getAvatarId()+"\");", u.getUsername(), u.getId(), u.getDisplayName())));
return "friends";
}
@ -63,6 +71,23 @@ public class StaticController {
return "settings";
}
@PostMapping(value = "/settings")
private String uploadImage(@RequestParam(required = true, name = "file-0") MultipartFile multipartImage) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if( authentication.isAuthenticated() ) {
try {
User u = userService.findByUsername(((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername());
Image dbImage = new Image();
dbImage.setContent(multipartImage.getBytes());
userService.setAvatar(u.getId(), imageRepository.save(dbImage)
.getId());
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
return "redirect:/settings";
}
@GetMapping("/")
public String homePage(Model model) {
AuthenticationUtil.authProfile(model, userService);

View File

@ -12,4 +12,5 @@ public class FriendPageResult {
private String avatar;
private String username;
private Long id;
private String displayName;
}

View File

@ -10,9 +10,11 @@ import lombok.NoArgsConstructor;
public class UserResult {
private Long id;
private String username;
private Long avatarId;
public boolean equals(UserResult r2) {
return this.getId().longValue() == r2.getId().longValue() &&
this.getUsername().equals(r2.getUsername());
this.getUsername().equals(r2.getUsername()) &&
this.getAvatarId().longValue() == r2.getAvatarId().longValue();
}
}

View File

@ -1,9 +1,6 @@
package com.alterdekim.game.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@ -21,5 +18,6 @@ public class Image {
Long id;
@Lob
@Column(length=16777211)
byte[] content;
}

View File

@ -31,6 +31,12 @@ public class User {
@Column(nullable = false)
private Long avatarId;
@Column(nullable = false)
private String displayName;
@Column(nullable = false)
private String pronouns;
@ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(
name="users_roles",

View File

@ -11,4 +11,17 @@ import org.springframework.transaction.annotation.Transactional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
@Transactional
@Modifying
@Query(value = "UPDATE User u SET u.avatarId = :avatarId WHERE u.id = :userId")
void updateAvatar(@Param("userId") Long userId, @Param("avatarId") Long avatarId);
@Transactional
@Modifying
@Query(value = "UPDATE User u SET u.displayName = :displayName, u.pronouns = :pronouns, u.username = :username WHERE u.id = :userId")
void updateProfileInfo(@Param("userId") Long userId,
@Param("displayName") String displayName,
@Param("username") String username,
@Param("pronouns") String pronouns);
}

View File

@ -31,7 +31,8 @@ public class UserServiceImpl implements UserService {
public void saveUser(UserDTO userDto) {
User user = new User();
user.setUsername(userDto.getUsername());
user.setPronouns("she/her");
user.setDisplayName(userDto.getUsername());
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
Role role = roleRepository.findByName("ROLE_ADMIN");
if(role == null){
@ -69,5 +70,13 @@ public class UserServiceImpl implements UserService {
role.setName("ROLE_ADMIN");
return roleRepository.save(role);
}
public void updateProfileInfo(Long userId, String displayName, String nickname, String pronouns) {
userRepository.updateProfileInfo(userId, displayName, nickname, pronouns);
}
public void setAvatar(Long userId, Long imageId) {
userRepository.updateAvatar(userId, imageId);
}
}

View File

@ -15,7 +15,7 @@ public class AuthenticationUtil {
if( authentication.isAuthenticated() ) {
try {
User u = userService.findByUsername(((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername());
model.addAttribute("profile", new FriendPageResult("/profile/" + u.getId(), "", u.getUsername(), u.getId()));
model.addAttribute("profile", new FriendPageResult("/profile/" + u.getId(), "/image/store/"+u.getAvatarId(), u.getUsername(), u.getId(), u.getDisplayName()));
return u;
} catch (Exception e) {
log.error(e.getMessage(), e);

View File

@ -111,7 +111,7 @@ function successPolling(data) {
let room_p_html = '';
for( let u = 0; u < room.players.length; u++ ) {
let room_player = room.players[u];
room_p_html += '<div class="games-room-one-body-members-one"><div class="games-room-one-body-members-one-avatar" style="background-image: url(&quot;https://i.dogecdn.wtf/7lurfckMFrYXm4gf&quot;);"><a href="/profile/'+room_player.id+'"></a><div class="_online"></div></div><div class="games-room-one-body-members-one-nick"><a href="/profile/'+room_player.id+'">'+room_player.username+'</a></div></div>';
room_p_html += '<div class="games-room-one-body-members-one"><div class="games-room-one-body-members-one-avatar" data-avatar-id="'+room_player.avatarId+'"><a href="/profile/'+room_player.id+'"></a><div class="_online"></div></div><div class="games-room-one-body-members-one-nick"><a href="/profile/'+room_player.id+'">'+room_player.username+'</a></div></div>';
}
for( let u = 0; u < (room.playerCount - room.players.length); u++ ) {
room_p_html += '<div data-room-id="'+room.id+'" onclick="joinRoom(this)" class="games-room-one-body-members-one _slot_join"><div class="games-room-one-body-members-one-avatar"><ion-icon name="add-outline" style="color: #656d78;"></ion-icon></div><div class="games-room-one-body-members-one-nick"><span>Join</span></div></div>';
@ -141,6 +141,11 @@ function successPolling(data) {
});
}
}
$(".games-room-one-body-members-one-avatar").each(function() {
if($(this).attr("data-avatar-id") != undefined) {
$(this).css("background-image", "url('/image/store/"+$(this).attr("data-avatar-id")+"')");
}
});
for( let i = 0; i < friends.length; i++ ) {
let friend = friends[i];
if( friend.action == 'ADD' ) {
@ -151,7 +156,7 @@ function successPolling(data) {
}
}
fids.push({id: friend.id, username: friend.username});
let fr_html = '<div class="friend-one" data-friend-id="'+friend.id+'"><a href="/profile/'+friend.id+'" class="navbar-btn"><img class="navbar-profile-img" src="https://avatars.githubusercontent.com/u/102559365?v=4"></a><span>'+friend.username+'</span><ion-icon onClick="sendInviteMessage('+friend.id+')" name="person-add" role="img" class="md hydrated"></ion-icon></div>';
let fr_html = '<div class="friend-one" data-friend-id="'+friend.id+'"><a href="/profile/'+friend.id+'" class="navbar-btn"><img class="navbar-profile-img" src="/image/store/'+friend.avatarId+'"></a><span>'+friend.username+'</span><ion-icon onClick="sendInviteMessage('+friend.id+')" name="person-add" role="img" class="md hydrated"></ion-icon></div>';
$(".friends-online-list").append(fr_html);
} else if( friend.action == 'REMOVE' ) {
for( let u = 0; u < fids.length; u++ ) {

View File

@ -0,0 +1,40 @@
function triggerFile(obj) {
$("#avatarFile").trigger('click');
}
function saveProfileInfo() {
let display_name = $("#displayname-input").val();
let nickname = $("#nickname-input").val();
let pronouns = $('#pronouns-select').find(":selected").text();
$.ajax({
url: "/api/v1/settings/profile/save",
data: {
display_name: display_name,
nickname: nickname,
pronouns: pronouns
},
method: "POST"
}).done(function() {
window.location.reload();
});
}
$(document).ready(function() {
$('#avatarFile').change(function(evt) {
var data = new FormData();
$.each($(this)[0].files, function(i, file) {
data.append('file-'+i, file);
});
$.ajax({
url: "/settings",
method: "POST",
cache: false,
contentType: false,
processData: false,
data: data
}).done(function() {
window.location.reload();
});
});
});

View File

@ -33,7 +33,7 @@
<img class="navbar-profile-img" th:src="${profile.avatar}">
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" th:text="${profile.username}"></a></li>
<li><a class="dropdown-item" th:text="${profile.displayName}"></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" th:href="${profile.href}">Profile</a></li>
<li><a class="dropdown-item" href="/settings">Settings</a></li>

View File

@ -17,7 +17,7 @@
</a>
<div class="friends-list-one-info">
<div class="friends-list-one-info-nick">
<a th:href="${friend.href}" th:text="${friend.username}"></a>
<a th:href="${friend.href}" th:text="${friend.displayName}"></a>
</div>
<div class="friends-list-one-info-actions" style="display: none;">
<button onClick="removeFriend(this)" class="btn btn-danger btn-sm" th:data-friend-id="${friend.id}">Remove</button>

View File

@ -15,27 +15,32 @@
<div class="grid">
<div class="g-col-4" style="display: flex; justify-content: center;">
<div style="width: 50%;">
<img class="profile-img" src="https://tg.dogecdn.wtf/v2/AgACAgIAAx0EWSIHJgAC0MBf64kG8Q99Gm69NezdtLUxTJaBiQACvrExG1sQWEtraYhjusGQAAGJdBmYLgADAQADAgADeAAD8RcFAAEeBA.jpg">
<button class="btn btn-primary" style="margin-top: 10px; font-size: 0.7rem;">Update avatar</button>
<img class="profile-img" th:src="${profile.avatar}">
<input type="file" id="avatarFile" accept="image/png, image/jpeg, image/gif" style="display: none;">
<button class="btn btn-primary" onClick="triggerFile(this)" style="margin-top: 10px; font-size: 0.7rem;">Update avatar</button>
</div>
</div>
<div class="g-col-8" style="padding: 5px;">
<div class="mb-3">
<label for="displayname-input" class="form-label">Display name (may contain unicode symbols)</label>
<input type="text" class="form-control" id="displayname-input" autocomplete="off">
<input type="text" th:value="${profile.displayName}" class="form-control" id="displayname-input" autocomplete="off">
</div>
<div class="mb-3">
<label for="nickname-input" class="form-label">Nickname</label>
<input type="text" class="form-control" id="nickname-input" autocomplete="off">
<input type="text" th:value="${profile.username}" class="form-control" id="nickname-input" autocomplete="off">
</div>
<div class="mb-3">
<label for="pronouns-select" class="form-label">Pronouns</label>
<select class="form-select" id="pronouns-select" aria-label="">
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
<option value="1" selected>she/her</option>
<option value="2">he/him</option>
<option value="3">they/them</option>
<option value="4">idc</option>
</select>
</div>
<div class="mb-3">
<button class="btn btn-primary" onClick="saveProfileInfo()">Save</button>
</div>
</div>
</div>
</div>
@ -90,5 +95,6 @@
</div>
</div>
<th:block th:insert="~{fragments/essentials}"></th:block>
<script src="/static/javascript/settings.js"></script>
</body>
</html>