diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java index 056df000b4..f6dc5f14b9 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java @@ -17,87 +17,20 @@ package bisq.desktop.components.cathash; -import bisq.common.util.MathUtils; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.Optional; @Slf4j -public class BucketConfig { +public abstract class BucketConfig { + static final String CURRENT_VERSION = "v1"; static final String DIGIT = "#"; static final String SHAPE_NUMBER = "#SHAPE_NUMBER#"; - private static final Bucket BG = new Bucket(16, 0); - private static final Bucket BG_OVERLAY = new Bucket(32, 1); - private static final Bucket BODY_AND_FACE = new Bucket(16, 2); - private static final Bucket CHEST_AND_EARS = new Bucket(16, 3); - private static final Bucket CHEST_OVERLAY = new Bucket(3, 4); - private static final Bucket EARS_OVERLAY = new Bucket(3, 5); - private static final Bucket FACE_OVERLAY = new Bucket(17, 6); - private static final Bucket EYES = new Bucket(16, 7); - private static final Bucket NOSE = new Bucket(6, 8); - private static final Bucket WHISKERS = new Bucket(7, 9); - private static final Bucket BODY_SHAPE = new Bucket(2, 10); - private static final Bucket CHEST_SHAPE = new Bucket(2, 11); - private static final Bucket EARS_SHAPE = new Bucket(2, 12); - private static final Bucket FACE_SHAPE = new Bucket(5, 13); + abstract int[] getBucketSizes(); - private static final int[] BUCKET_SIZES = new int[]{ - BG.getCount(), - BG_OVERLAY.getCount(), - BODY_AND_FACE.getCount(), - CHEST_AND_EARS.getCount(), - CHEST_OVERLAY.getCount(), - EARS_OVERLAY.getCount(), - FACE_OVERLAY.getCount(), - EYES.getCount(), - NOSE.getCount(), - WHISKERS.getCount(), - BODY_SHAPE.getCount(), - CHEST_SHAPE.getCount(), - EARS_SHAPE.getCount(), - FACE_SHAPE.getCount() - }; - - private static final PathDetails[] PATH_TEMPLATES; - - static { - String postFix = ".png"; - PATH_TEMPLATES = new PathDetails[]{ - new PathDetails("bg/bg_0/" + DIGIT + postFix, BG.getIdx()), - new PathDetails("bg/bg_1/" + DIGIT + postFix, BG_OVERLAY.getIdx()), - new PathDetails("body/body" + SHAPE_NUMBER + "/" + DIGIT + postFix, BODY_AND_FACE.getIdx(), BODY_SHAPE.getIdx()), - new PathDetails("chest/chest" + SHAPE_NUMBER + "_0/" + DIGIT + postFix, CHEST_AND_EARS.getIdx(), CHEST_SHAPE.getIdx()), - new PathDetails("chest/chest" + SHAPE_NUMBER + "_1/" + DIGIT + postFix, CHEST_OVERLAY.getIdx(), CHEST_SHAPE.getIdx()), - new PathDetails("ears/ears" + SHAPE_NUMBER + "_0/" + DIGIT + postFix, CHEST_AND_EARS.getIdx(), EARS_SHAPE.getIdx()), - new PathDetails("ears/ears" + SHAPE_NUMBER + "_1/" + DIGIT + postFix, EARS_OVERLAY.getIdx(), EARS_SHAPE.getIdx()), - new PathDetails("face/face" + SHAPE_NUMBER + "_0/" + DIGIT + postFix, BODY_AND_FACE.getIdx(), FACE_SHAPE.getIdx()), - new PathDetails("face/face" + SHAPE_NUMBER + "_1/" + DIGIT + postFix, FACE_OVERLAY.getIdx(), FACE_SHAPE.getIdx()), - new PathDetails("eyes/" + DIGIT + postFix, EYES.getIdx()), - new PathDetails("nose/" + DIGIT + postFix, NOSE.getIdx()), - new PathDetails("whiskers/" + DIGIT + postFix, WHISKERS.getIdx()) - }; - - long numCombinations = getNumCombinations(); - log.info("Number of combinations: 2^{} = {}", MathUtils.getLog2(numCombinations), numCombinations); - } - - static int[] getBucketSizes() { - return BUCKET_SIZES; - } - - static PathDetails[] getPathTemplates() { - return PATH_TEMPLATES; - } - - static long getNumCombinations() { - long result = 1; - for (int bucketSize : BUCKET_SIZES) { - result *= bucketSize; - } - return result; - } + abstract PathDetails[] getPathTemplates(); @Getter static class Bucket { diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfigV1.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfigV1.java new file mode 100644 index 0000000000..74eedfc31a --- /dev/null +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfigV1.java @@ -0,0 +1,86 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq 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 Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.cathash; + +import bisq.common.util.MathUtils; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class BucketConfigV1 extends BucketConfig { + private static final Bucket BG = new Bucket(16, 0); + private static final Bucket BG_OVERLAY = new Bucket(32, 1); + private static final Bucket BODY_AND_FACE = new Bucket(16, 2); + private static final Bucket CHEST_AND_EARS = new Bucket(16, 3); + private static final Bucket CHEST_OVERLAY = new Bucket(3, 4); + private static final Bucket EARS_OVERLAY = new Bucket(3, 5); + private static final Bucket FACE_OVERLAY = new Bucket(17, 6); + private static final Bucket EYES = new Bucket(16, 7); + private static final Bucket NOSE = new Bucket(6, 8); + private static final Bucket WHISKERS = new Bucket(7, 9); + private static final Bucket BODY_SHAPE = new Bucket(2, 10); + private static final Bucket CHEST_SHAPE = new Bucket(2, 11); + private static final Bucket EARS_SHAPE = new Bucket(2, 12); + private static final Bucket FACE_SHAPE = new Bucket(5, 13); + + private static final int[] BUCKET_SIZES = new int[]{ + BG.getCount(), + BG_OVERLAY.getCount(), + BODY_AND_FACE.getCount(), + CHEST_AND_EARS.getCount(), + CHEST_OVERLAY.getCount(), + EARS_OVERLAY.getCount(), + FACE_OVERLAY.getCount(), + EYES.getCount(), + NOSE.getCount(), + WHISKERS.getCount(), + BODY_SHAPE.getCount(), + CHEST_SHAPE.getCount(), + EARS_SHAPE.getCount(), + FACE_SHAPE.getCount() + }; + + private static final PathDetails[] PATH_TEMPLATES; + + static { + String postFix = ".png"; + PATH_TEMPLATES = new PathDetails[]{ + new PathDetails("bg/bg_0/" + DIGIT + postFix, BG.getIdx()), + new PathDetails("bg/bg_1/" + DIGIT + postFix, BG_OVERLAY.getIdx()), + new PathDetails("body/body" + SHAPE_NUMBER + "/" + DIGIT + postFix, BODY_AND_FACE.getIdx(), BODY_SHAPE.getIdx()), + new PathDetails("chest/chest" + SHAPE_NUMBER + "_0/" + DIGIT + postFix, CHEST_AND_EARS.getIdx(), CHEST_SHAPE.getIdx()), + new PathDetails("chest/chest" + SHAPE_NUMBER + "_1/" + DIGIT + postFix, CHEST_OVERLAY.getIdx(), CHEST_SHAPE.getIdx()), + new PathDetails("ears/ears" + SHAPE_NUMBER + "_0/" + DIGIT + postFix, CHEST_AND_EARS.getIdx(), EARS_SHAPE.getIdx()), + new PathDetails("ears/ears" + SHAPE_NUMBER + "_1/" + DIGIT + postFix, EARS_OVERLAY.getIdx(), EARS_SHAPE.getIdx()), + new PathDetails("face/face" + SHAPE_NUMBER + "_0/" + DIGIT + postFix, BODY_AND_FACE.getIdx(), FACE_SHAPE.getIdx()), + new PathDetails("face/face" + SHAPE_NUMBER + "_1/" + DIGIT + postFix, FACE_OVERLAY.getIdx(), FACE_SHAPE.getIdx()), + new PathDetails("eyes/" + DIGIT + postFix, EYES.getIdx()), + new PathDetails("nose/" + DIGIT + postFix, NOSE.getIdx()), + new PathDetails("whiskers/" + DIGIT + postFix, WHISKERS.getIdx()) + }; + } + + @Override + int[] getBucketSizes() { + return BUCKET_SIZES; + } + + @Override + PathDetails[] getPathTemplates() { + return PATH_TEMPLATES; + } +} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java index 4af1837820..f3d8a76475 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java @@ -34,26 +34,49 @@ public class CatHash { private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); public static Image getImage(UserProfile userProfile) { - return getImage(userProfile.getPubKeyHash(), userProfile.getProofOfWork().getSolution(), true); + return getImage(userProfile.getPubKeyHash(), userProfile.getProofOfWork().getSolution(), + userProfile.getAvatarVersion(), true); } - public static Image getImage(byte[] pubKeyHash, byte[] powSolution) { - return getImage(pubKeyHash, powSolution, true); + public static Image getImage(byte[] pubKeyHash, byte[] powSolution, String avatarVersion) { + return getImage(pubKeyHash, powSolution, avatarVersion, true); } - public static Image getImage(byte[] pubKeyHash, byte[] powSolution, boolean useCache) { + public static Image getImage(byte[] pubKeyHash, byte[] powSolution, String avatarVersion, boolean useCache) { byte[] combined = ByteArrayUtils.concat(powSolution, pubKeyHash); BigInteger input = new BigInteger(combined); if (useCache && CACHE.containsKey(input)) { return CACHE.get(input); } - int[] buckets = BucketEncoder.encode(input, BucketConfig.getBucketSizes()); - String[] paths = BucketEncoder.toPaths(buckets, BucketConfig.getPathTemplates()); + BucketConfig bucketConfig = getBucketConfig(avatarVersion); + log.info("Getting user avatar image using class {}", bucketConfig.getClass().getName()); + + int[] buckets = BucketEncoder.encode(input, bucketConfig.getBucketSizes()); + String[] paths = BucketEncoder.toPaths(buckets, bucketConfig.getPathTemplates()); Image image = ImageUtil.composeImage(paths, SIZE, SIZE); if (useCache && CACHE.size() < MAX_CACHE_SIZE) { CACHE.put(input, image); } return image; } + + public static String currentAvatarsVersion() { + return BucketConfig.CURRENT_VERSION; + } + + private static BucketConfig getBucketConfig(String avatarVersion) { + BucketConfig bucketConfig; + switch (avatarVersion) { + case "v1": { + bucketConfig = new BucketConfigV1(); + log.info("Creating v1 BucketConfig: {}", bucketConfig.getClass().getName()); + } + default: { + bucketConfig = new BucketConfigV1(); + log.info("Falling to default BucketConfig: {}", bucketConfig.getClass().getName()); + } + } + return bucketConfig; + } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/UserProfileDisplay.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/UserProfileDisplay.java index 6c5eae1dd4..143010624a 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/UserProfileDisplay.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/UserProfileDisplay.java @@ -56,6 +56,7 @@ public UserProfileDisplay(@Nullable UserProfile userProfile) { public UserProfileDisplay(@Nullable UserProfile userProfile, double size) { super(10); + setAlignment(Pos.CENTER_LEFT); userProfileIcon = new UserProfileIcon(size); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2Controller.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2Controller.java index 5efe9a4017..3a86bf3869 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2Controller.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2Controller.java @@ -38,6 +38,8 @@ @Slf4j public class CreateNewProfileStep2Controller implements InitWithDataController { + private static final String CURRENT_AVATARS_VERSION = CatHash.currentAvatarsVersion(); + @Getter @ToString @EqualsAndHashCode @@ -90,7 +92,7 @@ public void initWithData(InitData data) { model.setProofOfWork(data.getProofOfWork()); model.getNickName().set(data.getNickName()); model.getNym().set(data.getNym()); - model.getCatHashImage().set(CatHash.getImage(data.getPubKeyHash(), data.getProofOfWork().getSolution())); + model.getCatHashImage().set(CatHash.getImage(data.getPubKeyHash(), data.getProofOfWork().getSolution(), CURRENT_AVATARS_VERSION)); } @Override @@ -133,6 +135,7 @@ protected void onSave() { model.getKeyPair(), model.getPubKeyHash(), model.getProofOfWork(), + CURRENT_AVATARS_VERSION, model.getTerms().get(), model.getStatement().get()) .whenComplete((chatUserIdentity, throwable) -> UIThread.run(() -> { @@ -148,4 +151,4 @@ protected void onSave() { protected void close() { OverlayController.hide(); } -} \ No newline at end of file +} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2View.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2View.java index abd16fdc49..9f3b4126e6 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2View.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/user/user_profile/create/step2/CreateNewProfileStep2View.java @@ -128,5 +128,7 @@ protected void onViewDetached() { nym.textProperty().unbind(); terms.textProperty().unbindBidirectional(model.getTerms()); statement.textProperty().unbindBidirectional(model.getStatement()); + saveButton.setOnAction(null); + cancelButton.setOnAction(null); } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java index b7636073bb..cc64ca0560 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java @@ -46,6 +46,8 @@ @Slf4j public class CreateProfileController implements Controller { + private static final String CURRENT_AVATARS_VERSION = CatHash.currentAvatarsVersion(); + protected final CreateProfileModel model; @Getter protected final CreateProfileView view; @@ -119,6 +121,7 @@ protected void onCreateUserProfile() { model.getKeyPair().orElseThrow(), model.getPubKeyHash().orElseThrow(), model.getProofOfWork().orElseThrow(), + CURRENT_AVATARS_VERSION, "", "") .whenComplete((chatUserIdentity, throwable) -> UIThread.run(() -> { @@ -155,7 +158,7 @@ private CompletableFuture createProofOfWork(byte[] pubKeyHash) { model.setProofOfWork(Optional.of(proofOfWork)); byte[] powSolution = proofOfWork.getSolution(); String nym = NymIdGenerator.generate(pubKeyHash, powSolution); - Image image = CatHash.getImage(pubKeyHash, powSolution); + Image image = CatHash.getImage(pubKeyHash, powSolution, CURRENT_AVATARS_VERSION); model.getNym().set(nym); model.getCatHashImage().set(image); model.getPowProgress().set(0); diff --git a/user/src/main/java/bisq/user/identity/UserIdentityService.java b/user/src/main/java/bisq/user/identity/UserIdentityService.java index 54e52dcb5b..94be4e81b2 100644 --- a/user/src/main/java/bisq/user/identity/UserIdentityService.java +++ b/user/src/main/java/bisq/user/identity/UserIdentityService.java @@ -174,11 +174,12 @@ public CompletableFuture createAndPublishNewUserProfile(String nic KeyPair keyPair, byte[] pubKeyHash, ProofOfWork proofOfWork, + String avatarVersion, String terms, String statement) { String identityTag = nickName + "-" + Hex.encode(pubKeyHash); return identityService.createNewActiveIdentity(identityTag, keyPair) - .thenApply(identity -> createUserIdentity(nickName, proofOfWork, terms, statement, identity)) + .thenApply(identity -> createUserIdentity(nickName, proofOfWork, avatarVersion, terms, statement, identity)) .thenApply(userIdentity -> { publishPublicUserProfile(userIdentity.getUserProfile(), userIdentity.getIdentity().getNetworkIdWithKeyPair().getKeyPair()); return userIdentity; @@ -315,12 +316,14 @@ private CompletableFuture publishPublicUserProfile(UserProfile private UserIdentity createUserIdentity(String nickName, ProofOfWork proofOfWork, + String avatarVersion, String terms, String statement, Identity identity) { checkArgument(nickName.equals(nickName.trim()) && !nickName.isEmpty(), "Nickname must not have leading or trailing spaces and must not be empty."); - UserProfile userProfile = new UserProfile(nickName, proofOfWork, identity.getNetworkIdWithKeyPair().getNetworkId(), terms, statement); + UserProfile userProfile = new UserProfile(nickName, proofOfWork, avatarVersion, + identity.getNetworkIdWithKeyPair().getNetworkId(), terms, statement); UserIdentity userIdentity = new UserIdentity(identity, userProfile); synchronized (lock) { diff --git a/user/src/main/java/bisq/user/profile/UserProfile.java b/user/src/main/java/bisq/user/profile/UserProfile.java index 3de9b752b0..9f33bf4384 100644 --- a/user/src/main/java/bisq/user/profile/UserProfile.java +++ b/user/src/main/java/bisq/user/profile/UserProfile.java @@ -55,7 +55,8 @@ public final class UserProfile implements DistributedData { public static final int MAX_LENGTH_STATEMENT = 100; public static UserProfile from(UserProfile userProfile, String terms, String statement) { - return new UserProfile(userProfile.getNickName(), userProfile.getProofOfWork(), userProfile.getNetworkId(), terms, statement); + return new UserProfile(userProfile.getNickName(), userProfile.getProofOfWork(), userProfile.getAvatarVersion(), + userProfile.getNetworkId(), terms, statement); } // We give a bit longer TTL than the chat messages to ensure the chat user is available as long the messages are @@ -63,6 +64,7 @@ public static UserProfile from(UserProfile userProfile, String terms, String sta private final String nickName; // We need the proofOfWork for verification of the nym and cathash icon private final ProofOfWork proofOfWork; + private final String avatarVersion; private final NetworkId networkId; private final String terms; private final String statement; @@ -73,11 +75,13 @@ public static UserProfile from(UserProfile userProfile, String terms, String sta public UserProfile(String nickName, ProofOfWork proofOfWork, + String avatarVersion, NetworkId networkId, String terms, String statement) { this.nickName = nickName; this.proofOfWork = proofOfWork; + this.avatarVersion = avatarVersion; this.networkId = networkId; this.terms = terms; this.statement = statement; @@ -98,6 +102,7 @@ public bisq.user.protobuf.UserProfile toProto() { .setNickName(nickName) .setTerms(terms) .setStatement(statement) + .setAvatarVersion(avatarVersion) .setProofOfWork(proofOfWork.toProto()) .setNetworkId(networkId.toProto()) .build(); @@ -106,6 +111,7 @@ public bisq.user.protobuf.UserProfile toProto() { public static UserProfile fromProto(bisq.user.protobuf.UserProfile proto) { return new UserProfile(proto.getNickName(), ProofOfWork.fromProto(proto.getProofOfWork()), + proto.getAvatarVersion(), NetworkId.fromProto(proto.getNetworkId()), proto.getTerms(), proto.getStatement()); @@ -205,6 +211,7 @@ public String toString() { return "UserProfile{" + "\r\n nickName='" + nickName + '\'' + ",\r\n proofOfWork=" + proofOfWork + + ",\r\n avatarVersion=" + avatarVersion + ",\r\n networkId=" + networkId + ",\r\n terms='" + terms + '\'' + ",\r\n statement='" + statement + '\'' + @@ -213,4 +220,4 @@ public String toString() { ",\r\n bondedReputationHash=" + bondedReputationHash + "\r\n}"; } -} \ No newline at end of file +} diff --git a/user/src/main/proto/user.proto b/user/src/main/proto/user.proto index 1408cd3286..9196a7e852 100644 --- a/user/src/main/proto/user.proto +++ b/user/src/main/proto/user.proto @@ -30,6 +30,7 @@ message UserProfile { security.ProofOfWork proofOfWork = 3; string terms = 4; string statement = 5; + string avatarVersion = 6; } message UserIdentity {