From df87d53fd2044ea3306c45acb020237153a61a25 Mon Sep 17 00:00:00 2001 From: David Gerber Date: Mon, 30 Sep 2024 00:22:44 +0200 Subject: [PATCH] Improve contact view Add contacts list and searching. --- .../controller/contact/ContactController.java | 56 +++++++ .../io/xeres/app/service/ContactService.java | 56 +++++++ .../identity/item/IdentityGroupItem.java | 4 +- .../main/resources/application-dev.properties | 3 +- .../java/io/xeres/common/rest/PathConfig.java | 1 + .../io/xeres/common/rest/contact/Contact.java | 24 +++ .../io/xeres/ui/client/ContactClient.java | 59 ++++++++ .../ui/controller/contact/ContactCell.java | 59 ++++++++ .../contact/ContactViewController.java | 140 ++++++++++++++++++ .../debug/PropertiesWindowController.java | 3 +- .../resources/view/contact/contactview.fxml | 40 ++++- 11 files changed, 436 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/io/xeres/app/api/controller/contact/ContactController.java create mode 100644 app/src/main/java/io/xeres/app/service/ContactService.java create mode 100644 common/src/main/java/io/xeres/common/rest/contact/Contact.java create mode 100644 ui/src/main/java/io/xeres/ui/client/ContactClient.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/contact/ContactCell.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java diff --git a/app/src/main/java/io/xeres/app/api/controller/contact/ContactController.java b/app/src/main/java/io/xeres/app/api/controller/contact/ContactController.java new file mode 100644 index 000000000..6c9c01008 --- /dev/null +++ b/app/src/main/java/io/xeres/app/api/controller/contact/ContactController.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.api.controller.contact; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.service.ContactService; +import io.xeres.common.rest.contact.Contact; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static io.xeres.common.rest.PathConfig.CONTACT_PATH; + +@Tag(name = "Contact", description = "Contact service", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/contact", description = "Contact documentation")) +@RestController +@RequestMapping(value = CONTACT_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class ContactController +{ + private final ContactService contactService; + + public ContactController(ContactService contactService) + { + this.contactService = contactService; + } + + @GetMapping("") + @Operation(summary = "Get all the contacts") + @ApiResponse(responseCode = "200", description = "Request successful") + public List getContacts() + { + return contactService.getContacts(); + } +} diff --git a/app/src/main/java/io/xeres/app/service/ContactService.java b/app/src/main/java/io/xeres/app/service/ContactService.java new file mode 100644 index 000000000..98630eb49 --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/ContactService.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.xrs.service.identity.IdentityRsService; +import io.xeres.common.rest.contact.Contact; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class ContactService +{ + private final ProfileService profileService; + private final LocationService locationService; + private final IdentityRsService identityRsService; + + public ContactService(ProfileService profileService, LocationService locationService, IdentityRsService identityRsService) + { + this.profileService = profileService; + this.locationService = locationService; + this.identityRsService = identityRsService; + } + + @Transactional(readOnly = true) + public List getContacts() + { + var profiles = profileService.getAllProfiles(); + var identities = identityRsService.getAll(); + + // XXX: for now return all of them, in the future it would be possible to merge the identity to the profile (if the name is the same) + List contacts = new ArrayList<>(profiles.size() + identities.size()); + profiles.forEach(profile -> contacts.add(new Contact(profile.getName(), profile.getId(), 0L))); + identities.forEach(identity -> contacts.add(new Contact(identity.getName(), 0L, identity.getId()))); // XXX: put the profile too + return contacts; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/identity/item/IdentityGroupItem.java b/app/src/main/java/io/xeres/app/xrs/service/identity/item/IdentityGroupItem.java index 71cd17f2e..61a62c4d8 100644 --- a/app/src/main/java/io/xeres/app/xrs/service/identity/item/IdentityGroupItem.java +++ b/app/src/main/java/io/xeres/app/xrs/service/identity/item/IdentityGroupItem.java @@ -37,7 +37,7 @@ import static io.xeres.app.xrs.serialization.Serializer.*; @Entity(name = "identity_group") -public class IdentityGroupItem extends GxsGroupItem // XXX: beware because we need to be able to serialize just the group data (here) and the group metadata (superclass) +public class IdentityGroupItem extends GxsGroupItem { @Transient public static final IdentityGroupItem EMPTY = new IdentityGroupItem(); @@ -45,7 +45,7 @@ public class IdentityGroupItem extends GxsGroupItem // XXX: beware because we ne @Embedded @AttributeOverride(name = "identifier", column = @Column(name = "profile_hash")) private Sha1Sum profileHash; // hash of the gxsId + public key - private byte[] profileSignature; // XXX: warning, RS puts this in a string! we might have to do some serialization trickery... see p3idservice.cc in service_createGroup(), but I think my system's flexibility makes up for it + private byte[] profileSignature; @Transient private List recognitionTags = new ArrayList<>(); // not used (but serialized) diff --git a/app/src/main/resources/application-dev.properties b/app/src/main/resources/application-dev.properties index 32e616e4d..2cead72a7 100644 --- a/app/src/main/resources/application-dev.properties +++ b/app/src/main/resources/application-dev.properties @@ -89,9 +89,8 @@ springdoc.swagger-ui.tags-sorter=alpha ## Actuator info.java.vm.vendor=${java.vm.vendor} info.java.version=${java.version} -management.endpoints.jmx.exposure.exclude=* management.endpoint.shutdown.enabled=true -management.endpoints.web.exposure.include=info,health,env,logfile,shutdown +management.endpoints.web.exposure.include=* management.endpoints.web.base-path=/api/v1/actuator management.info.java.enabled=true management.info.os.enabled=true diff --git a/common/src/main/java/io/xeres/common/rest/PathConfig.java b/common/src/main/java/io/xeres/common/rest/PathConfig.java index 69228ca22..fd0b4adb4 100644 --- a/common/src/main/java/io/xeres/common/rest/PathConfig.java +++ b/common/src/main/java/io/xeres/common/rest/PathConfig.java @@ -39,4 +39,5 @@ private PathConfig() public static final String SHARES_PATH = "/api/v1/shares"; public static final String FILES_PATH = "/api/v1/files"; public static final String STATISTICS_PATH = "/api/v1/statistics"; + public static final String CONTACT_PATH = "/api/v1/contacts"; } diff --git a/common/src/main/java/io/xeres/common/rest/contact/Contact.java b/common/src/main/java/io/xeres/common/rest/contact/Contact.java new file mode 100644 index 000000000..ce9ed47a7 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/contact/Contact.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.contact; + +public record Contact(String name, long profileId, long identityId) +{ +} diff --git a/ui/src/main/java/io/xeres/ui/client/ContactClient.java b/ui/src/main/java/io/xeres/ui/client/ContactClient.java new file mode 100644 index 000000000..05cbd8e6a --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/ContactClient.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client; + +import io.xeres.common.events.StartupEvent; +import io.xeres.common.rest.contact.Contact; +import io.xeres.common.util.RemoteUtils; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import static io.xeres.common.rest.PathConfig.CONTACT_PATH; + +@Component +public class ContactClient +{ + private final WebClient.Builder webClientBuilder; + + private WebClient webClient; + + public ContactClient(WebClient.Builder webClientBuilder) + { + this.webClientBuilder = webClientBuilder; + } + + @EventListener + public void init(@SuppressWarnings("unused") StartupEvent event) + { + webClient = webClientBuilder + .baseUrl(RemoteUtils.getControlUrl() + CONTACT_PATH) + .build(); + } + + public Flux getContacts() + { + return webClient.get() + .uri("") + .retrieve() + .bodyToFlux(Contact.class); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/contact/ContactCell.java b/ui/src/main/java/io/xeres/ui/controller/contact/ContactCell.java new file mode 100644 index 000000000..3bd8bbb42 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/contact/ContactCell.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.contact; + +import io.xeres.common.rest.contact.Contact; +import io.xeres.ui.client.GeneralClient; +import io.xeres.ui.custom.AsyncImageView; +import javafx.scene.control.TableCell; +import javafx.scene.image.ImageView; + +import static io.xeres.common.rest.PathConfig.IDENTITIES_PATH; + +public class ContactCell extends TableCell +{ + private final GeneralClient generalClient; + + public ContactCell(GeneralClient generalClient) + { + super(); + this.generalClient = generalClient; + } + + @Override + protected void updateItem(Contact item, boolean empty) + { + super.updateItem(item, empty); + setText(empty ? null : item.name()); + setGraphic(empty ? null : updateContactImage((AsyncImageView) getGraphic(), item)); + } + + private ImageView updateContactImage(AsyncImageView imageView, Contact contact) + { + if (imageView == null) + { + imageView = new AsyncImageView(); + imageView.setFitWidth(32); + imageView.setFitHeight(32); + } + imageView.setUrl(contact.identityId() != 0 ? (IDENTITIES_PATH + "/" + contact.identityId() + "/image") : null, url -> generalClient.getImage(url).block()); + return imageView; + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java b/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java new file mode 100644 index 000000000..d525f3e23 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.contact; + +import atlantafx.base.controls.Tile; +import io.xeres.common.rest.contact.Contact; +import io.xeres.ui.client.ContactClient; +import io.xeres.ui.client.GeneralClient; +import io.xeres.ui.controller.Controller; +import io.xeres.ui.custom.AsyncImageView; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import net.rgielen.fxweaver.core.FxmlView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Locale; + +import static io.xeres.common.rest.PathConfig.IDENTITIES_PATH; + +@Component +@FxmlView(value = "/view/contact/contactview.fxml") +public class ContactViewController implements Controller +{ + private static final Logger log = LoggerFactory.getLogger(ContactViewController.class); + + @FXML + private TableView contactTableView; + + @FXML + private TableColumn contactTableNameColumn; + + @FXML + private TableColumn contactTablePresenceColumn; + + @FXML + private TextField searchTextField; + + @FXML + private Tile tileView; + + @FXML + private AsyncImageView contactImageView; + + @FXML + private Label type; + + private final ContactClient contactClient; + private final GeneralClient generalClient; + + public ContactViewController(ContactClient contactClient, GeneralClient generalClient) + { + this.contactClient = contactClient; + this.generalClient = generalClient; + } + + @Override + public void initialize() throws IOException + { + contactTableNameColumn.setCellFactory(param -> new ContactCell(generalClient)); + contactTableNameColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue())); + + var filteredList = new FilteredList<>(contactTableView.getItems()); + + contactClient.getContacts().collectList() + .doOnSuccess(contacts -> Platform.runLater(() -> { + // Add all contacts + contactTableView.getItems().addAll(contacts); + + // Sort by name + contactTableView.getSortOrder().add(contactTableNameColumn); + contactTableNameColumn.setSortType(TableColumn.SortType.ASCENDING); + contactTableNameColumn.setSortable(true); + })) + .subscribe(); + + searchTextField.textProperty().addListener((observable, oldValue, newValue) -> { + if (newValue.isEmpty()) + { + //noinspection unchecked + contactTableView.setItems((ObservableList) filteredList.getSource()); + } + else + { + if (!(contactTableView.getItems() instanceof FilteredList)) + { + contactTableView.setItems(filteredList); + } + filteredList.setPredicate(s -> s.name().toLowerCase(Locale.ROOT).contains(newValue.toLowerCase(Locale.ROOT))); + } + }); + + contactTableView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> changeSelectedContact(newValue)); + } + + private void changeSelectedContact(Contact contact) + { + if (contact == null) + { + tileView.setTitle(null); + tileView.setDescription(null); + contactImageView.setImage(null); + return; + } + + log.debug("Set contact to {}", contact); + tileView.setTitle(contact.name()); + tileView.setDescription(contact.name()); + // XXX: use a default avatar when it's not here! pretty much like in chat... need to use a stackpane or so... + contactImageView.setUrl(contact.identityId() != 0L ? (IDENTITIES_PATH + "/" + contact.identityId() + "/image") : null, url -> generalClient.getImage(url).block()); + type.setText(contact.identityId() != 0L ? "Contact" : "Profile"); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/debug/PropertiesWindowController.java b/ui/src/main/java/io/xeres/ui/controller/debug/PropertiesWindowController.java index 0aca2cafb..7768686c9 100644 --- a/ui/src/main/java/io/xeres/ui/controller/debug/PropertiesWindowController.java +++ b/ui/src/main/java/io/xeres/ui/controller/debug/PropertiesWindowController.java @@ -22,7 +22,6 @@ import io.xeres.ui.controller.WindowController; import io.xeres.ui.support.contextmenu.XContextMenu; import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; import javafx.scene.control.TableColumn; @@ -64,7 +63,7 @@ public void initialize() tableName.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getKey())); tableValue.setCellValueFactory(param -> new SimpleStringProperty(showLineSeparator(param.getValue().getValue()))); - propertiesTableView.getItems().addAll(FXCollections.observableArrayList(getSortedProperties().entrySet())); + propertiesTableView.getItems().addAll(getSortedProperties().entrySet()); propertiesTableView.getSortOrder().add(tableName); tableName.setSortType(ASCENDING); diff --git a/ui/src/main/resources/view/contact/contactview.fxml b/ui/src/main/resources/view/contact/contactview.fxml index d4f37a908..e9f79b701 100644 --- a/ui/src/main/resources/view/contact/contactview.fxml +++ b/ui/src/main/resources/view/contact/contactview.fxml @@ -17,7 +17,41 @@ ~ along with Xeres. If not, see . --> - - - + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+
+
\ No newline at end of file